Command and mission ingress semantic mismatches

PX4 accepts a command path, but the accepted path either loses a required command parameter or accepts non-finite navigation input before changing command state.

Tested environment:

PX4 SITL models: sihsim_quadx, sihsim_standard_vtol
PX4 version: v1.17.0
PX4 git hash: d6f12ad1c4f70ad3230afd7d86e971421e02fef4
MAVLink protocol: MAVLink 2

A. Accepted command ingress does not preserve or validate command semantics

A1. MAV_CMD_COMPONENT_ARM_DISARM: forced disarm works directly but not as an accepted mission item

Input:

Direct command:
  command: MAV_CMD_COMPONENT_ARM_DISARM
  param1: 0
  param2: 21196

Mission item:
  command: MAV_CMD_COMPONENT_ARM_DISARM
  frame: MAV_FRAME_GLOBAL_RELATIVE_ALT
  param1: 0
  param2: 21196
  z: 15

Reproduction steps:

  1. Start PX4 SITL with sihsim_quadx.
  2. Arm and take off to approximately 15 m.
  3. Send the direct forced-disarm command above.
  4. Take off again, upload a mission containing the same command and force parameter, switch to AUTO, and start the mission.

Observed direct-command behavior:

direct_command_accepted: true
direct_alt_before_m: 13.133
direct_alt_after_m: -1.035
direct_disarmed: true

Observed mission-item behavior:

mission_upload_accepted: true
mission_mode_after: AUTO_LOITER
mission_alt_after_m: 15.412
mission_disarmed: false
semantics_match: false

Expected behavior: if PX4 accepts this command as a mission item, the mission path should preserve the param2=21196 force flag, or reject the mission item as unsupported.

Actual behavior: the direct forced-disarm command succeeded, while the accepted mission item did not disarm. The observed behavior is consistent with the mission path losing the param2 force-disarm value.

Impact:

  • A mission can be accepted while containing an arming-state command whose execution semantics differ from the same direct MAVLink command.
  • Mission review can make the item appear equivalent to a direct forced disarm even though the mission execution path does not carry the force flag.
  • Because this command affects vehicle arming state, silent semantic loss is safety-relevant.

Relevant source:

src/modules/mavlink/mavlink_mission.cpp

1536: 		case MAV_CMD_COMPONENT_ARM_DISARM:
1537: 			mission_item->nav_cmd = (NAV_CMD)mavlink_mission_item->command;
1538: 			mission_item->params[0] = (uint16_t)mavlink_mission_item->param1;
1539: 			break;

src/modules/navigator/mission_block.cpp

533: 	// Mission item's NAV_CMD enums directly map to the according vehicle command
534: 	// So set the raw value directly (MAV_FRAME_MISSION mission item)
535: 	vehicle_command_s vehicle_command{};
536: 	vehicle_command.command = item.nav_cmd;
537: 	vehicle_command.param1 = item.params[0];
538: 	vehicle_command.param2 = item.params[1];
539: 	vehicle_command.param3 = item.params[2];
540: 	vehicle_command.param4 = item.params[3];
541: 	vehicle_command.param5 = static_cast<double>(item.params[4]);
542: 	vehicle_command.param6 = static_cast<double>(item.params[5]);
543: 	vehicle_command.param7 = item.params[6];

src/modules/commander/Commander.cpp

948: 	case vehicle_command_s::VEHICLE_CMD_COMPONENT_ARM_DISARM: {
949: 
950: 			// Adhere to MAVLink specs, but base on knowledge that these fundamentally encode ints
951: 			// for logic state parameters
952: 			const int8_t arming_action = static_cast<int8_t>(lroundf(cmd.param1));
953: 
954: 			if (arming_action != vehicle_command_s::ARMING_ACTION_ARM
955: 			    && arming_action != vehicle_command_s::ARMING_ACTION_DISARM) {
956: 				mavlink_log_critical(&_mavlink_log_pub, "Unsupported ARM_DISARM param: %.3f\t", (double)cmd.param1);
957: 				events::send<float>(events::ID("commander_unsupported_arm_disarm_param"), events::Log::Error,
958: 						    "Unsupported ARM_DISARM param: {1:.3}", cmd.param1);
959: 
960: 			} else {
961: 				// Arm is forced (checks skipped) when param2 is set to a magic number.
962: 				const bool forced = (static_cast<int>(lroundf(cmd.param2)) == 21196);
963: 
964: 				transition_result_t arming_res = TRANSITION_DENIED;
968: 				if (arming_action == vehicle_command_s::ARMING_ACTION_ARM) {
969: 					arming_res = arm(arm_disarm_reason, cmd.from_external || !forced);
970: 
971: 				} else if (arming_action == vehicle_command_s::ARMING_ACTION_DISARM) {
972: 					arming_res = disarm(arm_disarm_reason, forced);

The mission parser stores only param1 for the coordinate-frame mission item path. Navigator later republishes item.params[1] as vehicle_command.param2, and Commander uses param2 to decide whether the disarm is forced.

A2. MAV_CMD_NAV_VTOL_TAKEOFF: direct command with NaN position and altitude is ACK accepted

Input:

command: MAV_CMD_NAV_VTOL_TAKEOFF
param1: NaN
param5: NaN
param6: NaN
param7: NaN

Reproduction steps:

  1. Start PX4 SITL with sihsim_standard_vtol.
  2. Confirm the heartbeat is initially in AUTO loiter, not AUTO VTOL takeoff.
  3. Send the direct MAV_CMD_NAV_VTOL_TAKEOFF command above with NaN values in the required navigation fields.
  4. Observe the COMMAND_ACK and heartbeat custom mode.

Observed result:

nan_command_ack: MAV_RESULT_ACCEPTED
nan_entered_auto_vtol_takeoff: true
after_nan_main_mode: 4
after_nan_sub_mode: 10
nan_armed_seen: false
nan_alt_delta_m: -0.011000000000000003

Expected behavior: PX4 should reject a VTOL takeoff command when required position or altitude fields are NaN/non-finite, or explicitly sanitize those fields before accepting the command.

Actual behavior: PX4 returned MAV_RESULT_ACCEPTED and the heartbeat stream changed to AUTO VTOL takeoff (main_mode=4, sub_mode=10). In this SITL run the vehicle remained disarmed and did not climb, so this reproduction confirms acceptance and mode-state mutation, not completed takeoff.

Impact:

  • A sender receives an accepted result for a malformed takeoff command.
  • The malformed command can move PX4 into AUTO VTOL takeoff state even though its required navigation inputs are not finite.
  • This is safety-relevant command-state behavior, even though this specific run did not show arming or climb from the malformed input.

Relevant source:

src/modules/commander/Commander.cpp

1076: 	case vehicle_command_s::VEHICLE_CMD_NAV_VTOL_TAKEOFF:
1077: #if CONFIG_MODE_NAVIGATOR_VTOL_TAKEOFF
1078: 
1079: 		/* ok, home set, use it to take off */
1080: 		if (_user_mode_intention.change(vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF, getSourceFromCommand(cmd))) {
1081: 			cmd_result = vehicle_command_ack_s::VEHICLE_CMD_RESULT_ACCEPTED;
1082: 
1083: 		} else {
1084: 			printRejectMode(vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF);
1085: 			cmd_result = vehicle_command_ack_s::VEHICLE_CMD_RESULT_TEMPORARILY_REJECTED;
1086: 		}

src/modules/navigator/navigator_main.cpp

651: 			} else if (cmd.command == vehicle_command_s::VEHICLE_CMD_NAV_VTOL_TAKEOFF) {
652: 
653: 				_vtol_takeoff.setTransitionAltitudeAbsolute(cmd.param7);
654: 
655: 				// after the transition the vehicle will establish on a loiter at this position
656: 				_vtol_takeoff.setLoiterLocation(matrix::Vector2d(cmd.param5, cmd.param6));
657: 
658: 				// loiter height is the height above takeoff altitude at which the vehicle will establish on a loiter circle
659: 				_vtol_takeoff.setLoiterHeight(cmd.param1);

The direct command path accepts the VTOL takeoff mode transition without validating finite command coordinates or altitude. Navigator then stores param7, param5, param6, and param1 into the VTOL takeoff state.

Suggested fixes:

  • Preserve MAV_CMD_COMPONENT_ARM_DISARM.param2 when accepting the command as a mission item, or reject this mission-item form.
  • Reject direct MAV_CMD_NAV_VTOL_TAKEOFF when required numeric fields are NaN or otherwise non-finite.
  • Return an explicit negative ACK for malformed command input before changing AUTO submode state.