fix(heap): validate the block header in eos_realloc(), not just eos_free() - #77
Conversation
…ree()
The kernel allocator has a validation boundary in one of its two pointer-taking
entry points. eos_free() range-checks its argument against the heap;
eos_realloc() takes the same kind of pointer, derives a header from it, and
trusts whatever it finds:
block_header_t *block = (block_header_t *)((uint8_t *)ptr - HEADER_SIZE);
size_t current_usable = block->size - HEADER_SIZE;
if (new_size <= current_usable) return ptr;
Two consequences, both reproduced against the unmodified allocator:
1. A pointer that never came from this heap is read out of bounds for its
"header" and then handed straight back, as if it had been resized. eos_free()
rejects exactly that pointer.
2. `block->size - HEADER_SIZE` is unsigned. A header whose size field is below
HEADER_SIZE underflows current_usable to nearly SIZE_MAX, so every growth
request looks already satisfied and realloc returns the original short
buffer. The caller then writes new_size bytes into it. That turns a single
corrupted header byte -- the thing a prior small overflow leaves behind --
into an unbounded overflow, which is the opposite of what an allocator's
metadata checks are for.
Both paths now go through one block_from_ptr() helper: the pointer must lie
inside the heap, its header must be 8-byte aligned relative to the heap base,
and the size field must be at least MIN_BLOCK_SIZE and not claim to extend past
the end of the heap. eos_free() uses it too and gains the size checks it did
not have. An unrecognised pointer makes realloc return NULL and free a no-op,
which is what each already did for the cases it did check.
Verified: ctest 24/24, pytest 10 passed. tests/test_heap.c gains coverage for
both paths plus a grow-and-preserve case; against the unmodified heap.c it
fails on "realloc of a non-heap pointer must be rejected".
Note for review: PR embeddedos-org#60 also touches tests/test_heap.c (it does not touch
kernel/src/mem/heap.c). If that lands first this needs a trivial rebase in the
test file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
srpatcha
left a comment
There was a problem hiding this comment.
Verified locally against current master: merges clean, builds with no new errors, ctest 24/24.
The asymmetry this fixes is the real bug — eos_free range-checked its argument and eos_realloc did not, so the same pointer was trusted in one path and validated in the other. Routing both through block_from_ptr is the right shape, and it means the two cannot drift apart again.
The size check earns its place independently of the range check. block->size - HEADER_SIZE is unsigned, so a header claiming a size below HEADER_SIZE underflows to near SIZE_MAX, every growth request compares as already satisfied, and realloc hands back the short buffer — one corrupted header byte becoming an unbounded overflow in the caller. Good catch, and the test pins exactly that case rather than only the easy foreign-pointer one.
Also checked the tests fail for the right reasons and that the added validation does not reject legitimate pointers — the eos_malloc/eos_free round trip after the realloc cases covers that.
|
Hi EmbeddedOS Team, I wanted to follow up regarding my application and the technical assessment. Since completing the initial assessment, I’ve continued working across eOS, eBoot, and ebuild and have contributed substantially beyond the initial requested PRs. My GitHub profile is Kartikey1306, where the complete contribution history is available: eOS #50 — Merged: Device Tree parser bounds/safety fixes and fuzzing integration eBoot #38 — Merged: Fail-closed CRC verification when flash reads fail ebuild #48 — Merged: Track C/C++ header dependencies to prevent stale builds Overall, this is 23 PRs and 83 commits across the three repositories, with 10 PRs merged, 12 currently open, and 1 closed. The open PRs are currently showing 12/12 green CI, and I’ve received detailed maintainer reviews and verification throughout the work. The work has covered security, embedded systems, parser safety, cryptographic verification, OTA security, boot integrity, build systems, testing/fuzzing, CI, dependency verification, and reliability. Several of the findings were security-sensitive issues on trust paths where failure could incorrectly result in a successful verification or boot decision. Given the amount of work I’ve now contributed and the feedback from the maintainers, I wanted to ask about the actual role and next steps in the hiring process. Could you please let me know: What is the role/title and primary responsibility? Most importantly, I’d appreciate knowing whether, based on my contributions and the maintainer feedback, my profile aligns with what you’re looking for and whether we can proceed to an interview or technical discussion. I’m happy to walk through any of the PRs, security findings, implementation decisions, testing methodology, or the reasoning behind the issues I identified. GitHub: Kartikey1306 Best regards, |
The architectural problem
The kernel allocator has two entry points that take a caller-supplied pointer and derive allocator metadata from it. One validates that pointer; the other does not.
eos_free()range-checks its argument against the heap.eos_realloc()takes the same kind of pointer and trusts whatever it finds:Two consequences, both reproduced against the unmodified allocator:
1. A foreign pointer is read out of bounds, then handed back
The bytes before
ptrare read as a block header, and the pointer is returned as if it had been resized.eos_free()rejects exactly that pointer.2. A short header underflows the size check into an unbounded overflow
block->size - HEADER_SIZEis unsigned. A header whose size field is belowHEADER_SIZEunderflowscurrent_usableto nearlySIZE_MAX, so every growth request looks already satisfied:realloc returns the original short buffer and the caller writes
new_sizebytes into it. That turns a single corrupted header byte — precisely what a prior small overflow leaves behind — into an unbounded overflow, which is the opposite of what an allocator's metadata checks are for.The change
Both paths go through one
block_from_ptr()helper:MIN_BLOCK_SIZE,eos_free()uses it too and gains the size checks it did not have. An unrecognised pointer makesreallocreturnNULLandfreea no-op — which is what each already did for the cases it did check, so nothing that worked before changes behaviour.Verification
ctest --no-tests=errorpytest tests/tests/test_heap.cgains coverage for both paths plus a grow-and-preserve case (contents survive a move) and a check that the new validation does not reject legitimate pointers. Against the unmodifiedheap.cit fails onrealloc of a non-heap pointer must be rejected.Notes for review
tests/test_heap.c(it does not touchkernel/src/mem/heap.c). If that lands first this needs a trivial rebase in the test file only.master.coalesce()walksblock->nextwithout validating it, so a corrupted next pointer still escapes the heap. That is a larger change to the block-list invariant and wants its own PR.🤖 Generated with Claude Code