"Unreadable program headers" check is warning-only: e_phoff/e_phnum bounds and the bytes-actually-read (nbytes) bound are never enforced β unbounded wild kernel read from firstpage+e_phoff and parsing of uninitialized heap for short files
| Field | Value |
|---|---|
| ID | DF-2809 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-125 / CWE-457 |
| File | sys/kern/link_elf.c |
| Lines | 489-492 (deref :500/:505; nbytes :453) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass β confirms the kern_linker audit's cross-file note) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The if at :489-492 whose condition checks e_phentsize,
e_phoff+e_phnum*56 β€ PAGE_SIZE and β€ nbytes has a body containing ONLY
link_elf_error() β no error assignment, no goto (FreeBSD's counterpart
carries error=ENOEXEC; goto out). Consequently (a) phdr =
firstpage + e_phoff is dereferenced with an unbounded 64-bit attacker
offset; (b) for files shorter than a page, everything past nbytes in
the kmalloc'd-without-M_ZERO firstpage is stale heap parsed as
Ehdr/phdr content. This confirms the kern_linker audit's cross-file
observation (nbytes computed at :453, never enforced).
Threat model & preconditions
Root-supplied crafted module β kernel read at attacker-chosen 64-bit
offset (panic), or module acceptance decisions driven by stale kernel
heap. VERIFIED both variants on the stock kernel: wild-offset β
Fatal user address access from kernel mode ... Stopped at
link_elf_load_file+0x242; short-file β "not dynamically-linked"
decided from uninit heap after the warning printed. Patched: both
ENOEXEC.
Recommended fix
Add the missing error = ENOEXEC; goto out; after the
link_elf_error() call (fix.diff hunk 1).
Timeline
- 2026-08-31 Discovered during pass-2 audit of link_elf.c (GLM 5.3); both manifestations reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2809 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| gen_exec_ko.py | β | 6.3 KB | view raw | |
| build.sh | β | 105 B | view raw | |
| run.sh | β | 394 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| env.txt | β | 512 B | view raw | |
| fix.diff | β | 4.1 KB | view raw | |
| run_fix.log | β | 1.4 KB | view raw |
DF-2809 β "Unreadable program headers" check is warning-only β unbounded OOB read via e_phoff/e_phnum + first-page parsing never bounded by bytes actually read
Build
python3 gen_exec_ko.py phdr-wild.ko phdr-wild # 64-byte file, e_phoff = 0x300000000000 python3 gen_exec_ko.py phdr-short.ko phdr-short # 64-byte file, e_phoff=0x40, e_phnum=2 (phdrs live past EOF)
Run
scp -F dfbsd-qemu/config phdr-wild.ko phdr-short.ko dfbsd:/tmp/ dfbsd-qemu/vm.sh run_root 'kldload /tmp/phdr-wild.ko; echo RC=$?' dfbsd-qemu/vm.sh log 45
Expected (stock kernel, verified 2026-09-01)
kldload: Unreadable program headers <-- the check FIRES... Fatal user address access from kernel mode from kldload at ffffffff80627802 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x2801170e3000 Stopped at link_elf_load_file+0x242: movl (%rax),%edx <-- ...and is ignored
phdr-short: kldload: Unreadable program headers then a decision made from
UNINITIALIZED heap ("Object is not dynamically-linked" on the observed run β
the phdr entries past the 64 file bytes came from stale M_LINKER heap).
Expected (patched kernel)
kldload: Unreadable program headers, RC=1 (ENOEXEC), guest stays up
Why (root cause, sys/kern/link_elf.c)
449: firstpage = kmalloc(PAGE_SIZE, M_LINKER, M_WAITOK); /* NOT M_ZERO */
451: vn_rdwr(UIO_READ, vp, firstpage, PAGE_SIZE, 0, ..., &resid);
453: nbytes = PAGE_SIZE - resid; /* bytes actually read */
...
489: if (!((hdr->e_phentsize == sizeof(Elf_Phdr)) &&
490: (hdr->e_phoff + hdr->e_phnum*sizeof(Elf_Phdr) <= PAGE_SIZE) &&
491: (hdr->e_phoff + hdr->e_phnum*sizeof(Elf_Phdr) <= nbytes)))
492: link_elf_error("Unreadable program headers"); /* NO error=, NO goto */
...
500: phdr = (Elf_Phdr *) (firstpage + hdr->e_phoff); /* unbounded */
505: switch (phdr->p_type) { /* wild read */
The if-body at 492 contains ONLY the kprintf β the missing
error = ENOEXEC; goto out; (present in FreeBSD's link_elf.c) makes both
the <= PAGE_SIZE and the <= nbytes conditions decorative:
- e_phoff (64-bit, file-controlled) is added to the firstpage pointer and
dereferenced with no bound β arbitrary-offset kernel read (the
phdr-wild panic).
- For short files, everything past nbytes in the kmalloc'd page is
uninitialized heap (no M_ZERO) and is parsed as Ehdr/phdr content
(phdr-short), so load decisions are driven by stale kernel heap.
DF-2809 VERDICT β reproduced (wild kernel read panic; nbytes never enforced)
Status: reproduced. Stock guest kernel
DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 X86_64_GENERIC,
fresh vm.sh reset with-src before the run.
Evidence
phdr-wild (deterministic panic)
login: kldload: Unreadable program headers Fatal user address access from kernel mode from kldload at ffffffff80627802 Fatal trap 12: page fault while in kernel mode cpuid = 4 fault virtual address = 0x2801170e3000 fault code = supervisor read data, page not present Stopped at link_elf_load_file+0x242: movl (%rax),%edx
(run.log, panic.txt). The 64-byte module sets e_phoff = 0x300000000000.
The message ordering on the serial console is the smoking gun: the loader
printed "Unreadable program headers" (the check condition is TRUE:
0x300000000000 + 1*56 > PAGE_SIZE) and then walked straight into
phdr = firstpage + 0x300000000000 and dereferenced it. firstpage
(0xfffffe1170e3000) + 0x300000000000 wrapped to the user-range address
0x2801170e3000 β "Fatal user address access from kernel mode".
link_elf_load_file+0x242 is switch (phdr->p_type) at
sys/kern/link_elf.c:505.
phdr-short (uninitialized heap parsed as phdrs)
64-byte file with e_phoff=0x40, e_phnum=2: the check fires again
(0x40 + 2*56 = 0xb0 > nbytes = 0x40) and loading continues, parsing both
phdr entries out of the kmalloc'd-but-never-written tail of firstpage
(allocated WITHOUT M_ZERO at sys/kern/link_elf.c:449). Observed outcome:
kldload: Object is not dynamically-linked β a decision derived purely
from stale heap contents (nondeterministic across heap states; on a groomed
heap the stale bytes can equally produce PT_LOAD/PT_DYNAMIC entries feeding
the segs[] machinery). nbytes (sys/kern/link_elf.c:453) is computed and
used in the dead check only β it never bounds any parsing.
Root cause
sys/kern/link_elf.c:489-492 β the if body is only link_elf_error(...).
FreeBSD's link_elf.c has error = ENOEXEC; goto out; in this block;
DragonFly is missing them, disabling BOTH protections the condition
expresses (in-page bounds and short-file/bytes-read bounds).
Impact
Root-gated (kldload) crafted-module kernel read at an attacker-chosen 64-bit offset β panic (DoS). The uninit-heap parse additionally makes module acceptance depend on stale kernel memory (non-deterministic loads). Low severity per the project's root-supplied-input rubric (cf. DF-0060 Low).
Fix validation
fix.diff adds the missing { error = ENOEXEC; goto out; }. Patched kernel:
kldload /tmp/phdr-wild.ko β "kldload: Unreadable program headers",
RC=1, guest stays up. phdr-short likewise rejected. See run_fix.log.
Fix verification
fixedPatched kernel: both phdr-wild and phdr-short rejected with 'kldload: Unreadable program headers', RC=1, guest up; the check now actually terminates loading (run_fix.log).
['run_fix.log', 'fix_build.log']
Confirmed kernel references
Detail
Exploit chain
crafted .ko -> kldload(root) -> dead check prints but continues -> phdr = firstpage + attacker u64 e_phoff -> switch(phdr->p_type) reads unmapped kernel VA -> panic. Short-file variant: parsing driven by uninitialized M_LINKER heap.
Evidence (decisive lines)
["run.log: console sequence 'kldload: Unreadable program headers' immediately followed by 'Fatal user address access from kernel mode ... fault virtual address = 0x2801170e3000'", 'panic.txt', "run.log phdr-short section: 'Unreadable program headers' then 'Object is not dynamically-linked' from stale heap"]
PoC changes
authored fresh this pass (gen_exec_ko.py phdr-wild / phdr-short modes)
Verified recommended fix
add the missing 'error = ENOEXEC; goto out;' to the existing check body (fix.diff hunk 1)
Verdict
The program-header bounds check at link_elf.c:489-492 is warning-only (its body is just link_elf_error(); FreeBSD carries error=ENOEXEC/goto out). Reproduced both dead protections: (1) phdr-wild: a 64-byte module with e_phoff=0x300000000000 prints 'kldload: Unreadable program headers' and then faults reading phdr->p_type at firstpage+e_phoff ('Fatal user address access from kernel mode', Fatal trap 12, Stopped at link_elf_load_file+0x242 = switch(phdr->p_type) :505); (2) phdr-short: 64-byte file with e_phoff=0x40/e_phnum=2 is parsed past nbytes out of the kmalloc'd-but-unwritten (no M_ZERO, :449) first page - observed 'Object is not dynamically-linked' decided from stale heap. Root-gated DoS + nondeterministic loads; Low per project rubric (cf. DF-0060).
No comments yet.