β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5833

generic_elf_coredump note-size precompute race: LWP insertions between the DRYRUN and WRITE passes overflow the header reservation, the swallowed elf_putallnotes error leaves target->off past off_max, elf_puttextvp computes vnh_max from a size_t-underflowed (off_max - off), and cb_put_fp writes attacker-sized vn_hdr records past the kmalloc'd core-header buffer

Summary

generic_elf_coredump sizes the ELF core header with a DRYRUN pass, kmallocs the buffer, then regenerates in WRITE mode β€” but sigexit's p_token does not pin the LWP set across blocking (DragonFly releases all tokens when a thread blocks) and proc_stop(SCORE) pre-counts LSSLEEP siblings as stopped while still mid-syscall, so workers parked inside lwp_create can insert new LWPs between the passes. Each extra LWP makes WRITE need 800 more bytes than reserved; target_reserve returns error but still advances target->off past off_max; puthdr discards elf_putallnotes' return; elf_puttextvp computes vnh_max from a size_t-underflowed (off_max - off) and cb_put_fp writes one 120-byte vn_hdr per vnode-backed mapping past the buffer end with attacker-influenced contents (mmap address/length + VFS file handles); the each_segment error overwrite hides the failure. Unpriv multithreaded rig: park workers in lwp_create under memory pressure, fatal signal, timed drips β€” guest run (47 attempts) proved the LWP-set growth window (14-29 LWPs per dump, reservation wall hit mid-walk in one attempt) but the terminal underflow write did not fire (honest inconclusive/not-reproduced terminal); the overflow is unconditional in code once the delta exceeds the slack. On non-SMAP/SMEP guests a uid=0 route via neighboring-object smash is plausible. Propagate-error + never-underflow fixes proposed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5833 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 6.0 KB ↓ raw
fix.diff β€” 1.8 KB view raw
run.final.log β€” 2.9 KB view raw
eater.c β€” 4.1 KB view raw
victim.c β€” 8.6 KB view raw
dripper.c β€” 2.3 KB view raw
controller.sh β€” 5.1 KB view raw
parse_core.c β€” 2.7 KB view raw
parse2.c β€” 2.5 KB view raw
verdict.json β€” 3.7 KB view raw

DF-5833 PoC β€” coredump LWP-set race β†’ elf_puttextvp vnh_max underflow β†’ cb_put_fp heap overflow

Unprivileged (uid 1001) rig for the size-precompute race in generic_elf_coredump() (sys/kern/imgact_elf.c).

Components

file role
eater.c pins VM free pages below the vm_page_alloc floor (memory pressure park)
victim.c sleeper threads + worker threads looping lwp_create(2); modes: probe / run (SIGSEGV) / ckpt (sys_checkpoint trigger)
dripper.c pre-forked drip driver: frees memory in controlled bursts into the parked allocators at rig-controlled times
controller.sh orchestration: probe (layout measure) β†’ pad tuning β†’ attempt loop
parse_core.c core/ckpt image NT_PRSTATUS counter
parse2.c post-note region analyzer (vminfo/vn_count/siginfo/filehdr consistency + reservation-fit arithmetic)

Build (on guest)

cc -O2 -Wall -o eater eater.c
cc -O2 -Wall -o victim victim.c -lpthread
cc -O2 -Wall -o dripper dripper.c
cc -O2 -Wall -o parse_core parse_core.c
cc -O2 -Wall -o parse2 parse2.c

Run

sh controller.sh stage1 5   # benign: successful core with extra NT_PRSTATUS
sh controller.sh ckpt2 6    # reservation-overrun attempts (root first: sysctl -w kern.ckptgroup=-1)

Each attempt (~20-90 s): eater pins free memory β†’ victim releases workers whose lwp_create calls park mid-creation β†’ victim triggers the core dump (SIGSEGV, or sys_checkpoint for the no-prelude path) β†’ the dumper's DRYRUN sizes the header β†’ dripper feeds pages β†’ parked lwp_create calls complete and insert LWPs into the tree while the dump is between its two passes β†’ the WRITE pass needs more note space than reserved.

Expected observations (see run.final.log)

  • children counts grow across snapshot β†’ DRYRUN β†’ WRITE (LWP insertion mid-dump): "race += 14..29" per attempt
  • successful images whose WRITE_end_raw consumes the reservation to its final bytes (delta absorbed by round_page slack)
  • CKPT-RESULT rv=-1 errno=22 with no image when the delta exceeds the slack (reservation wall hit mid-note-walk; error swallowed by puthdr)
  • (code-certain, not yet observed on-guest) with β‰₯~7 simultaneous in-window inserts: elf_puttextvp's vnh_max size_t-underflows and cb_put_fp writes vn_hdr records past the kmalloc'd header buffer.
VERDICT.md
↓ download raw

DF-5833 β€” coredump note-size precompute race β†’ swallowed putallnotes error β†’ size_t-underflowed vnh_max β†’ unbounded cb_put_fp heap overflow

Field Value
Finding LWP-set mutation between generic_elf_coredump's DRYRUN sizing pass and WRITE pass
File sys/kern/imgact_elf.c
Lines 1012/1023 (two-pass sizing), 1321-1322 (swallowed error), 1422 (in-window kmalloc drops p_token), 1653-1655 (vnh_max underflow), 1113-1185 (cb_put_fp writes)
Bucket memcorrupt
Guest DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), QEMU/KVM 4GB/6vCPU

What the bug is (code chain, all verified line-by-line)

  1. generic_elf_coredump() sizes the core header with a DRYRUN pass (__elfN(puthdr)(…, DRYRUN, …), imgact_elf.c:1012), allocates target.buf = kmalloc(target.off_max, M_TEMP, M_WAITOK|M_ZERO) (imgact_elf.c:1023) and then re-runs the header generation in WRITE mode (imgact_elf.c:1025 β†’ :1293).
  2. elf_putallnotes() sizes its NT_PRSTATUS/NT_FPREGSET notes from the live LWP set (FOREACH_LWP_IN_PROC, imgact_elf.c:1480). The two passes run at different times; in WRITE mode the pass starts with a fresh kmalloc(sizeof(*tmpdata), M_TEMP, M_ZERO|M_WAITOK) (imgact_elf.c:1422). sigexit() holds p->p_token across the dump (kern_sig.c:2385-2423), but DragonFly LWKT tokens are released whenever the holding thread blocks (lwkt_switch β†’ lwkt_relalltokens, lwkt_thread.c:641-643) β€” so every blocking point between the two walks (the off_max kmalloc, the tmpdata kmalloc, any fp_write) opens a window in which a sibling LWP can mutate the LWP tree. proc_stop(SCORE)'s pre-counted "stopped" LSSLEEP LWPs (kern_sig.c:1598-1612) are exactly such siblings: they are mid-syscall and continue running when woken.
  3. If LWPs are inserted between the passes (lwp_create β†’ lwp_rb_tree_RB_INSERT, kern_fork.c:848), the WRITE pass needs 800 bytes/LWP more than reserved. target_reserve() returns NULL, sets *error = EINVAL, but still advances target->off (imgact_elf.c:1268-1274), pushing off past off_max.
  4. __elfN(puthdr) discards elf_putallnotes()'s return value (imgact_elf.c:1321-1322) and keeps going.
  5. elf_puttextvp() then computes fpc.vnh_max = fpc.vnh + (target->off_max - target->off)/sizeof(vn_hdr) (imgact_elf.c:1653-1655). With off > off_max the subtraction size_t-underflows, making vnh_max astronomically large, and each_segment(p, cb_put_fp, &fpc, 0) writes one 120-byte vn_hdr per vnode-backed mapping starting past the end of the kmalloc'd header buffer (imgact_elf.c:1113-1185). The write length is attacker-chosen (number of mmap'd-file segments), the record contents are attacker-influenced (p_vaddr/p_filesz are the mmap addresses/sizes; the file handle comes from VFS_VPTOFH of attacker-created files).
  6. Bonus bug on the same path: error = each_segment(...) at imgact_elf.c:1656 overwrites the reserve-failure EINVAL, so elf_puttextvp returns success even after the overrun.

What was reproduced on the guest (uid 1001, unprivileged)

Rig: eater (pins VM free pages below the allocation floor), victim (200 sleeper threads + 16 worker threads looping lwp_create(2) so their calls park mid-creation on M_WAITOK allocations under the floor), dripper (pre-forked; frees memory in controlled bursts into the parked allocators), controller.sh (orchestrates probe/tune/attempt loops). Trigger: SIGSEGV path (initially) and sys_checkpoint(CKPT_FREEZE) (kern.ckptgroup set to -1 by root for rig determinism β€” the unprivileged sigexit path drives the identical generic_elf_coredump code).

Reproduced, 20+ times across runs (run.final.log): - The LWP set grows during the dump. Worker threads pre-counted as "stopped" by proc_stop(SCORE) keep completing lwp_create calls and inserting LWPs throughout the dump: children counts grow from the pre-dump snapshot β†’ DRYRUN β†’ WRITE β†’ post-dump by 14-29 per attempt (e.g. attempt 1: snapshot 3586, post-dump 3639, WRITE notes 3822 vs 3803 expected pre-dump LWPs). - The two sizing passes see different LWP sets. A fully-parsed "successful" image (parse2 output) shows WRITE_end_raw consuming the DRYRUN reservation down to its final 112 bytes β€” i.e. a DRYRUNβ†’WRITE delta of 1-6 notes absorbed by the round_page slack; deltas beyond the slack were observed as: - CKPT-RESULT rv=-1 errno=22 (EINVAL), no image (ckpt2 v2 attempt 4, call-time 93 children / 3 in-flight workers): the WRITE pass hit the reservation wall mid-walk β€” the size-precompute mismatch materialized as the swallowed-error failure. - The endgame heap-overflow write itself (delta β‰₯ ~7 simultaneous in-window inserts) did not demonstrably fire in the rig's attempts: the header-allocation page starvation during the DRYRUNβ†’WRITE window and the p_token endgame race bias the dumper on this configuration. The chain up to and including the precondition (mid-dump LWP insertion + reservation overrun) is reproduced; the unbounded write is established by line-level analysis (imgact_elf.c:1653-1655 arithmetic is unconditional).

PoC files

  • eater.c, victim.c, dripper.c, controller.sh, parse_core.c, parse2.c β€” the full rig (build: cc -O2 -o X X.c [-lpthread])
  • run.final.log β€” the final 6-attempt run + image parse analysis
  • fix.diff β€” error propagation + underflow guard

Fix validation

Not performed (kernel rebuild): the heap-overflow write did not demonstrate on-guest within the rig, so there is no observable regression to A/B. The fix.diff removes both enabling defects (swallowed error; underflow-prone bound) by construction.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix not build-validated: the terminal overflow write was not observed on-guest, so no A/B regression exists; the patch removes both enabling defects (error swallowing; underflow-prone bound) by construction.

findings/poc/DF-5833/fix.diff
↓ fix.diffper-fix-DF-5833

Confirmed kernel references

Detail

Evidence (decisive lines)

run.final.log (per-attempt children growth 14-29 across snapshot/DRYRUN/WRITE/post-dump; parse2 analysis showing WRITE_end_raw consuming the reservation to its final 112 bytes; earlier rv=-1/EINVAL attempt with no image); VERDICT.md (full chain with path:line); fix.diff

PoC changes

Original analysis had no PoC; the full rig (eater/victim/dripper/controller/parsers) was authored during verification. Multiple iterations fixed: lwp_create stack-top ABI (EFAULT), worker-thread lifetime, objcache drain depth, OOM-killer windows, pre-forked drip driver, checkpoint trigger for prelude-free DRYRUN timing, and per-attempt arithmetic.

Verified recommended fix

Propagate elf_putallnotes() failure in __elfN(puthdr) and guard elf_puttextvp's vnh_max against off > off_max (see fix.diff)

Verdict

The size-precompute race's preconditions are reproduced on the guest (20+ times): proc_stop(SCORE)-pre-counted worker LWPs keep completing lwp_create and inserting into the LWP tree throughout generic_elf_coredump, so the DRYRUN sizing pass and the WRITE pass observe different LWP sets (children grow 14-29 per attempt; one run aborted with rv=-1/EINVAL when the WRITE pass exhausted the reservation mid-note-walk, the swallowed-error failure mode). The terminal heap-overflow write (elf_putallnotes error swallowed at imgact_elf.c:1321 -> target_reserve advances off past off_max -> (off_max-off)/sizeof(vn_hdr) size_t-underflow at imgact_elf.c:1653 -> cb_put_fp writes vn_hdr records past the kmalloc'd header buffer) is proven by line-level analysis but did not demonstrably fire in the rig's attempts: the header-allocation page starvation during the DRYRUN->WRITE window and the p_token endgame race biased the dumper on this configuration, and the children-count variance randomizes the round_page slack. Classified inconclusive: race precondition + reservation-overrun failure reproduced; the unbounded write itself unobserved.