Skip to content

Fix two frame-allocator soundness bugs and a kernel-stack overrun - #72

Draft
chbaker0 wants to merge 2 commits into
masterfrom
claude/zen-goodall-k6w1xy
Draft

Fix two frame-allocator soundness bugs and a kernel-stack overrun#72
chbaker0 wants to merge 2 commits into
masterfrom
claude/zen-goodall-k6w1xy

Conversation

@chbaker0

Copy link
Copy Markdown
Owner

Filed by an AI coding agent (Claude Code).

Found during a scheduled security scan of the repo. Three bugs, all of which break the unsafe trait FrameAllocator invariant "allocations do not return allocated or reserved frames" or write outside an allocation. Each is covered by a regression test that fails on master.

1. find_bit_group tested 3 bits instead of 4 (shared/src/memory/alloc/phys.rs:364)

let mask = ((len << 1) - 1) as u8;

This is 2*len - 1, not 2^len - 1. It coincides with the intended value for len 1 and 2, but for len == 4 it produces 0b111 — so allocate_range(2) accepted a group where only 3 of the 4 frames were free, then cleared all 4 bits and handed out a frame that was already allocated.

29 of the 256 possible bitmap bytes trigger it. Example: 0b01110000 has frames 4,5,6 free and frame 7 in use; the old code returned Some(4) and handed out frames 4–7.

Reachable in the live kernel: mm::HeapProvider::allocate computes order from num_chunks.next_power_of_two(), so a heap allocation spanning 3–4 chunks calls allocate_range(2).

Fixed to ((1u16 << len) - 1) as u8.

2. fill_bitmap_from_map mishandled ranges inside a single byte (shared/src/memory/alloc/phys.rs:299)

The leading/trailing partial-byte logic assumed a range spans at least one 8-frame boundary. For an Available range entirely inside one bitmap byte, both partial branches fired and marked frames free that weren't in the map at all:

  • Frames 9–10 → marked frames 8–15 free (6 frames that aren't available).
  • Frames 1–2 → additionally computes end_aligned - 1 with end_aligned == 0, i.e. a u64 underflow (panics with overflow-checks, and with them off the following assert_eq! fails anyway).

Small conventional-memory regions of a few pages are ordinary in real UEFI memory maps, so this is reachable on real firmware rather than only in theory.

Rewrote the loop in terms of the inclusive last frame, with an explicit same-byte case. All pre-existing fill_bitmap_* tests still pass unchanged.

3. Kernel stacks were 4× smaller than STACK_LEN claims (src/sched.rs)

Three numbers disagreed:

frames
create_taskallocate_owned_frames(1) 2
STACK_FRAMES = 2 << STACK_FRAMES_ORDER (order 2) 8
STACK_LEN = STACK_FRAMES * PAGE_SIZE 32 KiB

create_task allocates 2 frames (8 KiB) but sets stack_top = stack_bottom + STACK_LEN (32 KiB), so every task's Task descriptor and initial stack contents land 24 KiB past the end of its own allocation — into whatever the frame allocator handed out next. This is on the live boot path (init_kernel_main_threadcreate_task, plus every spawn_kthread).

2 << order is also 2^(order+1), not 2^order — the frame allocator's actual contract.

Fixed by making STACK_FRAMES_ORDER the single source of truth (= 3), STACK_FRAMES = 1 << STACK_FRAMES_ORDER, and having create_task pass STACK_FRAMES_ORDER. STACK_LEN stays 32 KiB, so observable stack size is unchanged; the allocation now matches it. Added a const_assert_eq! tying the two together.

This is the one judgment call in the PR: preserving STACK_LEN was the choice that keeps behavior stable, but you could equally have wanted 4 frames (keeping STACK_FRAMES_ORDER = 2 and shrinking STACK_LEN to 16 KiB). Worth a look.

Test plan

  • cargo stest — 58 pass (54 pre-existing + 4 new). All 4 new tests fail on master; verified individually before fixing.
  • cargo kcheck / lcheck / icheck — clean.
  • cargo kclippy / sclippy — clean (no denied-lint violations).
  • Not run: a QEMU boot. qemu-system-x86_64 isn't available in this container, so bug 3's fix is unverified on real boot — it's the change most worth confirming that way, since it alters what the very first create_task allocates. CI's smoke job covers it.
  • Not run: cargo smiri (CI's expensive job covers it; none of these changes touch the paging pointer walks).

Generated by Claude Code

claude added 2 commits August 12, 2026 13:14
find_bit_group's mask was (len << 1) - 1 instead of (1 << len) - 1, and
fill_bitmap_from_map mishandled Available ranges contained in one bitmap byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgAWuxTU8rZUj7r9Eicwq1
create_task allocated 2 frames but placed the initial stack contents at
stack_bottom + STACK_LEN (8 frames), writing 24 KiB past the allocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgAWuxTU8rZUj7r9Eicwq1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants