reliable 26-2/26-3: zero the reassembly buffer, terminate config.name - #66
Conversation
…curity#26-3 name termination Co-Authored-By: DeepSeek worker (nova-swarm) <noreply@mas-bandwidth.com>
…me (security#26-2, security#26-3) Co-Authored-By: DeepSeek worker (nova-swarm) <noreply@mas-bandwidth.com>
|
Security seat second eyes at exact head f8d8ded (Alex; these are my security#26 findings 2 and 3, dispositioned this morning as optional non-defects — this PR hardens both). APPROVE — both fixes are the right shapes, and the tests are genuinely red-first quality.
Cost check: one memset per reassembly allocation, bounded by the endpoint's own config — negligible. Built and ran the suite at this head on this bench: cmake Release, 100% of 7 ctest targets pass (including the two new tests). One housekeeping note for the record: the alloc-null check this stacks on (reliable#63) was my #26 finding 1 — so this PR completes the disposition of all three findings from the reliable audit. |
Read (Fable) at f8d8dedVerdict: APPROVE. Cold read of the diff against main (reliable.c only, +137). Built Debug + security#26-2 — zero the reassembly buffer (fix at reliable.c:1450)Finding: "Reassembly buffers use malloc, not calloc ... any future logic error that skipped storing a fragment would leak heap contents into a delivered packet. Zeroing would be cheap defense-in-depth."
security#26-3 — terminate config.name (fix at reliable.c:718)Finding: "Latent over-read on a non-NUL-terminated config.name: reliable_printf("[%s]", config.name) reads past the 256-byte array if a caller fills all 256 bytes ... Trivial to fix with a forced NUL."
FindingsMEDIUM — residual over-read on the create-time error paths. LOW — LOW — behaviour change for correct callers: one memset of up to LOW — STANDARD.md: no sentence on reassembly buffer contents or the config name (grep: reassembl, config.name, zero); nothing to update. Counts: HIGH 0, MEDIUM 1, LOW 3. |
Read (Opus) at f8d8dedVerdict: HOLD. Cold read on a second model; no other reader's comment read first. Clean clone, 26-2 checks out mechanically: MEDIUM — security#26-3 is not actually closed:
|
reliable_config_valid runs before reliable_endpoint_create copies the config and terminates its own name, so its twelve rejection logs print the caller's buffer, which is not required to be NUL terminated. A caller that fills all 256 bytes with no NUL made those logs read past the array, which is the over-read the terminator closed for every later use of the name. Every one of those twelve logs now prints the name with a "%.*s" width of RELIABLE_MAX_NAME_CHARS, taken from sizeof the array so it cannot drift, and the allocation-failure log prints endpoint->config.name, the copy create has already terminated. test_config_name_bounded_in_rejection_log builds an invalid config on the heap with no zero byte in it, so an unbounded "%s" on the name leaves the allocation, and asserts the logged name stops at the last byte of the array. Red before the fix under ASan as a heap-buffer-overflow READ in reliable_printf, and as a failed length check in a plain Debug build; green after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Read (Fable) at a38f88cVerdict: APPROVE. Re-gate after the follow-up commit closed the f8d8ded residual. Whole diff read against origin/main (three commits: 39dbc19 red tests, f8d8ded fixes, a38f88c bounded logging). Fresh clone, security#26 item 2 — "Reassembly buffers use malloc, not calloc ... any future logic error that skipped storing a fragment would leak heap contents into a delivered packet. Zeroing would be cheap defense-in-depth."
security#26 item 3 — "reliable_printf("[%s]", config.name) reads past the 256-byte array if a caller fills all 256 bytes ... Trivial to fix with a forced NUL."
Every remaining
Build and tests at tip (sanitizers on, clean-first): 0 warnings under Red proofs (one revert each, clean rebuild, run,
No behaviour change for valid configs: LOW (notes, nothing to change):
|
Read (Opus) at a38f88cVerdict: APPROVE. Second model, cold: whole diff against origin/main, no other reader's comment read first. The residual my f8d8ded read held on — the rejection logs and the alloc-failure log printing the caller's unterminated security#26-3, the residual. The audit: "Latent over-read on a non-NUL-terminated config.name: reliable_printf("[%s]", config.name) reads past the 256-byte array if a caller fills all 256 bytes."
My earlier PoC, re-run. Same shape: 256 bytes of Red-first, all three tests. Reverted one site's width — caught by security#26-2. The audit asked for zeroing "particularly given the (currently unreachable) early-return paths in reliable_store_fragment_data". The memset at :1457 uses the same Build. LOW — notes, not change requests.
|
Merge on the record: macOS legs substituted by local macOS runsAt a38f88c eight hosted checks pass and the two macOS-15 legs (Debug, Release) have sat queued at zero seconds for fifty minutes on GitHub's macOS runner pool. Glenn, live at 23:58Z: "go ahead and merge with the local macOS runs on the record." The substitute, run on the Studio (macOS, Apple clang), at this exact head, by the two cold readers: Debug with This is an exception named per Glenn's flexibility rule, not the path; the hosted macOS legs will run on main after the merge and are read back there. |
Bump the two version sites for the 1.4.5 release: project(reliable VERSION) in CMakeLists.txt and RELIABLE_VERSION_FULL / RELIABLE_VERSION_PATCH in reliable.h. The release carries security#26-2 (the fragment reassembly buffer is zeroed after allocation) and security#26-3 (endpoint->config.name is terminated at create and the create-time logs print the caller's name bounded), merged in mas-bandwidth#66. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two small hardening fixes from the security review (security#26-2 and security#26-3), each with a red-first test.
security#26-2: zero the fragment reassembly buffer
reliable_endpoint_receive_packetallocates the reassembly buffer through the caller-suppliedallocate_function, which is malloc-shaped and returns uninitialized memory. Completeness isguaranteed today by the fragment bitmap plus validated offsets, so uninitialized bytes cannot reach
the process callback through correct code. But any future logic error that skipped storing a fragment
would leak stale heap contents into a delivered packet. The buffer is now
memsetto zero forpacket_buffer_sizebytes immediately after the NULL check succeeds.The caller's allocator is untouched: the allocation is still
allocate_function, notcalloc, andthe caller's allocator is not bypassed.
test_fragment_reassembly_buffer_zeroeddrives a fragmented packet through a receiver whoseallocator fills fresh memory with
0xCC, delivers only the first fragment, and asserts theuntouched tail byte of the reassembly buffer is zero. Red before the fix (the tail was
0xCC),green after.
security#26-3: terminate
config.namereliable_printf("[%s]", endpoint->config.name)reads past the 256-byte array if a caller fills all256 bytes with no NUL.
reliable_endpoint_createnow forces the last byte of its own copy ofconfig.nameto'\0'right after the copy, so every use of the name after create is bounded bythat terminator. The create-time error paths run before that copy and print the caller's own buffer:
the twelve rejection logs in
reliable_config_validnow print the name with a%.*swidth ofsizeof(config.name) - 1, and the allocation-failure log prints the endpoint's terminated copy.Caller-controlled only.
test_endpoint_name_terminatedcreates an endpoint from a config whose name is 256 non-NUL bytesand asserts the endpoint's own copy is NUL-terminated at the last byte. Red before the fix, green
after.
test_config_name_bounded_in_rejection_loghands an invalid config with a256-byte non-NUL name, sized on the heap so the over-read leaves the allocation, and asserts the
logged name stops at 255 characters. Red before the fix as an ASan heap-buffer-overflow READ in
reliable_printf(and as a failed length check without a sanitizer), green after.Gates
Run under
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DRELIABLE_SANITIZE=ON:cmake -B build -DCMAKE_BUILD_TYPE=Debug -DRELIABLE_SANITIZE=ON— configuredcmake --build build --parallel— builtctest --test-dir build --output-on-failure— 100% tests passed, 0 failed out of 7./build/bin/fuzz 2000000— completed, exit 0python3 tools/conformance/verify_standard.py— 2608 checks, 0 failures