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

"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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2809 Β· 10 files
FileTypeDescriptionSize
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
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.

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 15:20:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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).