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)
PoC verification
Evidence pack
findings/poc/DF-5833 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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)
generic_elf_coredump()sizes the core header with a DRYRUN pass (__elfN(puthdr)(β¦, DRYRUN, β¦), imgact_elf.c:1012), allocatestarget.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).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 freshkmalloc(sizeof(*tmpdata), M_TEMP, M_ZERO|M_WAITOK)(imgact_elf.c:1422).sigexit()holdsp->p_tokenacross 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 (theoff_maxkmalloc, thetmpdatakmalloc, 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.- 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 advancestarget->off(imgact_elf.c:1268-1274), pushingoffpastoff_max. __elfN(puthdr)discardself_putallnotes()'s return value (imgact_elf.c:1321-1322) and keeps going.elf_puttextvp()then computesfpc.vnh_max = fpc.vnh + (target->off_max - target->off)/sizeof(vn_hdr)(imgact_elf.c:1653-1655). Withoff > off_maxthe subtraction size_t-underflows, makingvnh_maxastronomically large, andeach_segment(p, cb_put_fp, &fpc, 0)writes one 120-bytevn_hdrper 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).- Bonus bug on the same path:
error = each_segment(...)at imgact_elf.c:1656 overwrites the reserve-failure EINVAL, soelf_puttextvpreturns 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 analysisfix.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_testableFix 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
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.
No comments yet.