Reproduction environment
Vehicle: ArduCopter SITL
ArduPilot baseline: ArduPilot-4.7 branch snapshot / 4.7.0 beta-era build
Tested commit: 97775f82cec5bd50296c523be9baa0dacef9bb9e
Commit summary: Plane: version to 4.7.0-beta7
SITL command shape: build/sitl/bin/arducopter --wipe --speedup 8 --model quad --defaults Tools/autotest/default_params/copter.parm
MAVLink: MAVLink 2, ardupilotmega dialect
Safety/authority guard bypasses
1. MAV_CMD_DEBUG_TRAP: debug trap can terminate the autopilot process even when addressed to another component
Input tested
COMMAND_LONG command=42700 MAV_CMD_DEBUG_TRAP target_component=42 params=[32451,0,0,0,0,0,0].
Reproduction steps
- Start a fresh ArduCopter SITL instance.
- Send
MAV_CMD_DEBUG_TRAPwith the magic value32451totarget_component=42. - Observe command ACK behavior and process liveness.
Observed behavior
- The client did not receive a normal command ACK.
- The MAVLink connection was lost immediately after sending the command.
- The client log records
sitl_exit_code=-5.
Expected behavior
A debug trap should not be reachable in normal operation, and it should not affect the autopilot when addressed to a different component.
Impact
A MAVLink sender can terminate the running autopilot process in SITL by sending the magic debug-trap command.
Code evidence
libraries/GCS_MAVLink/GCS_Common.cpp:5119-5128: The handler checks only the magic value and then callshal.util->trap().
5119: MAV_RESULT GCS_MAVLINK::handle_command_debug_trap(const mavlink_command_int_t &packet)
5120: {
5121: // magic number must be supplied to trap; you must *really* mean it.
5122: if (uint32_t(packet.param1) != 32451) {
5123: return MAV_RESULT_DENIED;
5124: }
5125: if (hal.util->trap()) {
5126: return MAV_RESULT_ACCEPTED;
5127: }
5128: return MAV_RESULT_UNSUPPORTED;
libraries/GCS_MAVLink/GCS_Common.cpp:5675-5676:MAV_CMD_DEBUG_TRAPdispatches to this handler.
5675: case MAV_CMD_DEBUG_TRAP:
5676: return handle_command_debug_trap(packet);
2. MAV_CMD_DO_SET_MODE: legacy safety base-mode path accepts remote safety-switch changes
Input tested
COMMAND_LONG command=176 MAV_CMD_DO_SET_MODE params=[128,0,0,0,0,0,0] and params=[128,1,0,0,0,0,0], where 128 is MAV_MODE_FLAG_DECODE_POSITION_SAFETY.
Reproduction steps
- Start a fresh ArduCopter SITL instance.
- Send
MAV_CMD_DO_SET_MODEwith base modeMAV_MODE_FLAG_DECODE_POSITION_SAFETYand custom mode 1 to force safety on. - Send
MAV_CMD_DO_MOTOR_TESTwith direct PWM input as a behavior check. - Send
MAV_CMD_DO_SET_MODEwith the same base mode and custom mode 0 to force safety off. - Send the same motor-test command again.
- Send
MAV_CMD_DO_SET_MODEwith custom mode 1 again and repeat the motor-test command.
Observed behavior
- The initial safety-on command returned
MAV_RESULT_ACCEPTED, and the following motor-test command returnedMAV_RESULT_FAILED. - After the safety-off command returned
MAV_RESULT_ACCEPTED, the same motor-test command returnedMAV_RESULT_ACCEPTED. - After forcing safety on again, the same motor-test command returned
MAV_RESULT_FAILEDagain. - This confirms that the command path did not only acknowledge receipt; in SITL it changed the HAL safety state enough to alter a safety-gated behavior.
Expected behavior
Remote mode-setting should not expose hardware safety-switch operations without explicit authorization and guard checks.
Impact
A MAVLink mode command can exercise the safety-switch path remotely. In SITL, this changes the safety state used by motor-test gating. On hardware with safety-switch support, the same code path maps to force_safety_on() and force_safety_off(), which are primary motor-output safety operations.
Code evidence
libraries/GCS_MAVLink/GCS_Common.cpp:5092-5097:handle_command_do_set_mode()casts command parameters and calls_set_mode_common().
5092: MAV_RESULT GCS_MAVLINK::handle_command_do_set_mode(const mavlink_command_int_t &packet)
5093: {
5094: const uint8_t _base_mode = (uint8_t)packet.param1;
5095: const uint32_t _custom_mode = (uint32_t)packet.param2;
5096:
5097: return _set_mode_common(_base_mode, _custom_mode);
libraries/GCS_MAVLink/GCS_Common.cpp:2892-2922:_set_mode_common()acceptsMAV_MODE_FLAG_DECODE_POSITION_SAFETYand callsforce_safety_off()orforce_safety_on().
2892: MAV_RESULT GCS_MAVLINK::_set_mode_common(const uint8_t _base_mode, const uint32_t _custom_mode)
2893: {
2894: // only accept custom modes because there is no easy mapping from Mavlink flight modes to AC flight modes
2895: #if AP_VEHICLE_ENABLED
2896: if ((_base_mode & MAV_MODE_FLAG_CUSTOM_MODE_ENABLED) != 0) {
2897: if (!AP::vehicle()->set_mode(_custom_mode, ModeReason::GCS_COMMAND)) {
2898: // often we should be returning DENIED rather than FAILED
2899: // here. Perhaps a "has_mode" callback on AP_::vehicle()
2900: // would do?
2901: return MAV_RESULT_FAILED;
2902: }
2903: return MAV_RESULT_ACCEPTED;
2904: }
2905: #endif
2906:
2907: if (_base_mode == MAV_MODE_FLAG_DECODE_POSITION_SAFETY) {
2908: // set the safety switch position. Must be in a command by itself
2909: if (_custom_mode == 0) {
2910: // turn safety off (pwm outputs flow to the motors)
2911: hal.rcout->force_safety_off();
2912: return MAV_RESULT_ACCEPTED;
2913: }
2914: if (_custom_mode == 1) {
2915: // turn safety on (no pwm outputs to the motors)
2916: if (hal.rcout->force_safety_on()) {
2917: return MAV_RESULT_ACCEPTED;
2918: }
2919: return MAV_RESULT_FAILED;
2920: }
2921: return MAV_RESULT_DENIED;
2922: }
libraries/GCS_MAVLink/GCS_Common.cpp:5717-5718:MAV_CMD_DO_SET_MODEdispatches to this handler.
5717: case MAV_CMD_DO_SET_MODE:
5718: return handle_command_do_set_mode(packet);
libraries/AP_HAL_SITL/RCOutput.h:22-39: In SITL,force_safety_on()andforce_safety_off()update the stored safety state.
22: /*
23: force the safety switch on, disabling PWM output from the IO board
24: */
25: bool force_safety_on(void) override {
26: safety_state = AP_HAL::Util::SAFETY_DISARMED;
27: return true;
28: }
29: /*
30: force the safety switch off, enabling PWM output from the IO board
31: */
32: void force_safety_off(void) override {
33: safety_state = AP_HAL::Util::SAFETY_ARMED;
34: }
35:
36: /*
37: get safety switch state, used by Util.cpp
38: */
39: AP_HAL::Util::safety_state _safety_switch_state(void) { return safety_state; }
libraries/AP_HAL_SITL/Util.cpp:89-97: The SITL utility safety-state query returns theRCOutputsafety state.
89: enum AP_HAL::Util::safety_state HALSITL::Util::safety_switch_state(void)
90: {
91: #define HAL_USE_PWM 1
92: #if HAL_USE_PWM
93: return ((RCOutput *)hal.rcout)->_safety_switch_state();
94: #else
95: return SAFETY_NONE;
96: #endif
97: }
ArduCopter/GCS_MAVLink_Copter.cpp:734-747:MAV_CMD_DO_MOTOR_TESTdispatches tomavlink_motor_test_start().
734: MAV_RESULT GCS_MAVLINK_Copter::handle_MAV_CMD_DO_MOTOR_TEST(const mavlink_command_int_t &packet)
735: {
736: // param1 : motor sequence number (a number from 1 to max number of motors on the vehicle)
737: // param2 : throttle type (0=throttle percentage, 1=PWM, 2=pilot throttle channel pass-through. See MOTOR_TEST_THROTTLE_TYPE enum)
738: // param3 : throttle (range depends upon param2)
739: // param4 : timeout (in seconds)
740: // param5 : num_motors (in sequence)
741: // param6 : motor test order
742: return copter.mavlink_motor_test_start(*this,
743: (uint8_t)packet.param1,
744: (uint8_t)packet.param2,
745: packet.param3,
746: packet.param4,
747: (uint8_t)packet.x);
ArduCopter/motor_test.cpp:127-131: Motor test is rejected whenhal.util->safety_switch_state()reportsSAFETY_DISARMED.
127: // check if safety switch has been pushed
128: if (hal.util->safety_switch_state() == AP_HAL::Util::SAFETY_DISARMED) {
129: gcs_chan.send_text(MAV_SEVERITY_CRITICAL,"%s: Safety switch", mode);
130: return false;
131: }
ArduCopter/motor_test.cpp:143-161: If the safety checks pass, motor test returns success and emits the normal start path.
143: // mavlink_motor_test_start - start motor test - spin a single motor at a specified pwm
144: // returns MAV_RESULT_ACCEPTED on success, MAV_RESULT_FAILED on failure
145: MAV_RESULT Copter::mavlink_motor_test_start(const GCS_MAVLINK &gcs_chan, uint8_t motor_seq, uint8_t throttle_type, float throttle_value,
146: float timeout_sec, uint8_t motor_count)
147: {
148: if (motor_count == 0) {
149: motor_count = 1;
150: }
151: // if test has not started try to start it
152: if (!ap.motor_test) {
153: /* perform checks that it is ok to start test
154: The RC calibrated check can be skipped if direct pwm is
155: supplied
156: */
157: if (!mavlink_motor_control_check(gcs_chan, throttle_type != 1, "Motor Test")) {
158: return MAV_RESULT_FAILED;
159: } else {
160: // start test
161: gcs().send_text(MAV_SEVERITY_INFO, "starting motor test");
Suggested fixes
- Gate debug/trap commands behind an explicit debug build or runtime option and strict target checks.
- Avoid exposing safety-switch operations through legacy mode fields unless an explicit authorization path is used.
- Return failure when a safety-affecting command is unavailable, not permitted, or not addressed to the autopilot component.