Skip to content

[Security] cfe/CF — 15 novel findings in cf_cfdp_r / cf_codec / cf_cmd / cf_cfdp (3 HIGH including remote-reachable wire-decode OOB read) #499

Description

@GaltRanch

Summary

Fifteen findings in nasa/CF (CFDP File Delivery Protocol) — the CCSDS file-transfer engine that runs over the cFE Software Bus on every cFS-using mission. Three confirmed HIGH severity (one of which involves a remote attacker on the file-transfer wire), eight MEDIUM, three LOW, one downgraded HIGH→MEDIUM after closer review.

The findings were produced by an LLM-driven source-aware audit of the receive/encode pipeline (cf_cfdp_r.c, cf_codec.c, headers) and the command-handler / orchestration layer (cf_cmd.c, cf_cfdp.c). The deterministic SAST pattern library used in the prior cFS audit captured a different (and broader) set of pattern hits in this codebase — 150 candidates over the same files — but did not capture any of the fifteen below. These require cross-function flow reasoning, recognizing wire-derived length-vs-buffer mismatches, or seeing where a future-refactor footgun is wired into a current-correct call site.

Severity overview

# Severity Location Class
1 HIGH cf_cfdp_r.c CF_CFDP_R_ProcessFd (~L113 region) CWE-195 signed/unsigned silent short-write
2 HIGH cf_cfdp_r.c CF_CFDP_R2_GapCompute CWE-191 integer underflow in NAK segment math
3 MEDIUM cf_cfdp_r.c R2_NakResponseSegment CWE-191 max_chunks-1 underflow when count=0
4 MEDIUM cf_codec.c CF_CFDP_DecodeAllSegments CWE-787 OOB write on > CF_PDU_MAX_SEGMENTS
5 HIGH cf_codec.c CF_CFDP_DecodeHeader CWE-130 wire-controlled variable-length decode
6 MEDIUM cf_cfdp_r.c CRC chunk loop CWE-195 + CWE-190 size_t overflow on fsize near SIZE_MAX
7 MEDIUM cf_cfdp_r.c R_CheckState_VALIDATE CWE-358 state-machine skips CRC when cached_pos==fsize early
8 LOW cf_codec.c CF_CFDP_EncodeFileDataHeader CWE-682 6-bit FSV silent truncation
9 MEDIUM cf_cmd.c CF_DoChanAction CWE-393 OR-combined enum, downgraded
10 MEDIUM cf_cmd.c CF_GetSetParamCmd CWE-754 index-before-bounds-check
11 MEDIUM cf_cfdp.c ProcessPlaybackDirectory CWE-119 snprintf precision vs buffer arithmetic
12 MEDIUM cf_cmd.c CF_WriteQueueCmd CWE-823 OOB pointer-arithmetic before bounds-check
13 MEDIUM cf_cfdp.c CF_CFDP_SetPduLength CWE-190 uint16 truncation of encoder position
14 LOW cf_cfdp.c CF_CFDP_PlaybackDir CWE-704 cross-function trust on chan
15 LOW cf_cfdp.c CF_CFDP_CheckAckNakCount CWE-190 uint8 counter wrap defeats ack_limit

The HIGH on cf_codec.c CF_CFDP_DecodeHeader is the most consequential — it's reachable from any remote CFDP peer (typical operational deployment: spacecraft↔ground link or inter-spacecraft DTN). The others are reachable from ground commands or local config corruption.

Highlights

#1 HIGH — cf_cfdp_r.c CF_CFDP_R_ProcessFd Signed/unsigned mismatch in CF_WrappedWrite return-vs-length check

fret is int32 (signed) but fd->data_len is size_t (unsigned). If CF_WrappedWrite returns a negative error (e.g. -1), the comparison fret != fd->data_len undergoes usual arithmetic conversions and the negative is promoted to a very large unsigned value. The comparison still trips (so the error path is taken) — but the diagnostic logged and the bytes-written count propagated downstream is wrong, and a downstream path that uses fret arithmetically before checking sign treats the write as having advanced by ~4 GiB.

CFDP supports a "large-file" extended-length mode (the engine even sets the flag in the codec). A remote peer sending file-data PDUs with offset > INT32_MAX (legal in large-file mode) triggers fret = CF_WrappedLseek(...) truncation in the prior call.

Fix. Change fret to int64 (or unify with size_t and check explicitly for (int64)fret < 0 first), and clamp / refuse offsets that exceed the underlying filesystem's offset range.

#2 HIGH — cf_cfdp_r.c CF_CFDP_R2_GapCompute Integer underflow in NAK segment math

pseg->offset_start = chunk->offset - nak->scope_start;   /* unsigned subtraction */
pseg->offset_end   = pseg->offset_start + chunk->size;

No check that chunk->offset >= nak->scope_start. The current SendNak caller always uses scope_start = 0 so it's safe today; the function is a generic callback also wired from other paths. If scope_start ever exceeds a chunk's offset (likely when a sender-influenced MD/EOF size value drives scope_start above an existing chunk), the receiver emits NAK PDUs with wildly wrong segment-request ranges. Downstream the sender may then resend at gigabyte-sized offsets or, when arithmetic overflows on the sender side, panic the transaction.

Fix.

if (chunk->offset < nak->scope_start) return; /* or clamp + warn */
pseg->offset_start = chunk->offset - nak->scope_start;

#5 HIGH — cf_codec.c CF_CFDP_DecodeHeader Wire-controlled variable-length field decode without remaining-buffer bound

eid_length and txn_seq_length come from the CFDP PDU header. The decode validates eid_length > sizeof(plh->source_eid) and txn_seq_length > sizeof(plh->sequence_num) — but sizeof(source_eid) and sizeof(sequence_num) are the local 8-byte types (uint64), so the validation accepts any eid_length ≤ 8. The decode then calls CF_DecodeIntegerInSize for source_eid, sequence_num, destination_eid, each advancing the decode position by the wire-declared length without checking that the remaining buffer holds that many bytes.

A peer sends a short PDU advertising large eid_length / txn_seq_length but truncated payload. CF_CFDP_DecodeHeader returns CFE_SUCCESS with header_encoded_length reflecting the maximum position — the dispatcher then trusts those lengths and the subsequent file-data parsing reads past the PDU buffer end.

Exploit path. Reachable from any remote CFDP peer with attacker-influenced PDU bytes. Not a hypothetical: CFDP is the file-transfer protocol used to receive operational files (config tables, sequences, firmware deltas) on the spacecraft from the ground. A compromised ground station or man-in-the-middle on the link is the threat actor.

Fix. Bound the variable-length decodes against CF_CODEC_GET_REMAIN(state):

if (plh->eid_length + plh->txn_seq_length + plh->eid_length > CF_CODEC_GET_REMAIN(state)) {
    return CF_ERROR;
}

#4 MEDIUM — cf_codec.c CF_CFDP_DecodeAllSegments OOB write when peer sends > CF_PDU_MAX_SEGMENTS

CF_CFDP_DecodeAllSegments writes into plseg->segments[num_segments] and increments num_segments. The bounds check if (plseg->num_segments >= CF_PDU_MAX_SEGMENTS) CF_CODEC_SET_DONE(state) is present but only takes effect at the START of the next iteration — the else path is taken first and writes into segments[num_segments] BEFORE the next-iteration check fires. On the first iteration that would exceed CF_PDU_MAX_SEGMENTS, the write lands at segments[CF_PDU_MAX_SEGMENTS] — one past the array end.

Fix. Move the bounds check INSIDE the while condition, or before the write:

while (limit > 0 && CF_CODEC_GET_REMAIN(state) != 0) {
    if (plseg->num_segments >= CF_PDU_MAX_SEGMENTS) {
        CF_CODEC_SET_DONE(state);
        break;
    }
    /* now safe to write segments[num_segments] */
}

#9 MEDIUM (was HIGH, downgraded after review) — cf_cmd.c CF_DoChanAction OR-combine of channel-action returns

if (data->byte[0] == CF_ALL_CHANNELS) {
    for (i = 0; i < CF_NUM_CHANNELS; ++i)
        ret |= fn(i, context);
}

The OR pattern is correct AS LONG AS CF_ChanAction_Status_SUCCESS == 0 and _ERROR != 0 (the standard convention). If a future caller adds a "warning" or "partial-success" enum value with overlapping bits, the OR could mask intent. Today this is a future-maintenance hazard rather than a bug.

(The original LLM-driven scan rated this HIGH; on close review the current code is correct. Reported as MEDIUM/maintenance-grade so a future refactor adding new enum states is forewarned.)

All 15 findings

The full text of each finding — including root cause, quoted source proof, and exploit hypothesis — is in the cf-all-novel.json artifact bundled with this report. The above five are the ones I would expect a maintainer to triage first.

Reproduction notes

For findings #1, #2, #5, #4 the proof is in the cited functions' source on the current main branch. For findings #3, #6, #7, #11, #13, #15 the exploit path requires either operator-set parameters out of design range (config table corruption, SetParam validation gaps) or a multi-step transaction state. For findings #10, #12, #14 the current call paths are safe but the patterns are footguns for a future refactor (cross-function trust without local validation).

Building a runnable CFDP PoC for any of the remote-attacker findings requires the cFS topology with cf app loaded plus a CFDP peer; out of scope for this static audit but recommended for the maintainers' triage pass.

Provenance

Discovered by Inquisitor, AstroLexis's autonomous source + binary security agent. The audit pipeline:

  1. KCode (https://github.com/AstrolexisAI/KCode) — deterministic SAST pattern library. kcode audit apps/cf/fsw/src --skip-verify --json returned 150 candidates over the files in scope. Pattern coverage matched zero of the fifteen findings below — every one of these required cross-function flow / wire-protocol reasoning the library does not encode.
  2. Inquisitor SourceHunter / novel-pattern-hunt — LLM (Claude Opus 4.7) reading two focused source bundles (parser: cf_cfdp_r.c, cf_codec.c, headers; cmd: cf_cfdp.c, cf_cmd.c, headers), instructed to look for vulnerability classes NOT covered by the deterministic library. Returned the fifteen findings above with file:line anchors and quoted proof for each.

Per nasa/CF SECURITY.md AI policy: discovery and report drafting used AI assistance. Discovery: Inquisitor (KCode static pass + LLM novel-pattern pass). Report drafted with AI assistance. All fifteen findings are anchored in concrete source; finding #9 was downgraded after manual review.

Best regards,

Bruno Aiub · AstroLexis · Inquisitor
contact@astrolexis.space

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions