Heap overflow via unchecked p_filesz > p_memsz in PT_LOAD segment loading
| Field | Value |
|---|---|
| ID | DF-0056 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/kern/link_elf.c |
| Lines | 507-563 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
link_elf_load_file never validates p_filesz <= p_memsz for PT_LOAD segments.
The kmalloc'd buffer (mapsize) is derived from p_memsz (:541-543), but
vn_rdwr (:554) writes p_filesz bytes into it. If p_filesz > p_memsz, the
write overflows. Immediately after, bzero(segbase + p_filesz, p_memsz -
p_filesz) (:562-563) underflows (both Elf64_Xword/uint64) to a huge value,
zeroing far past the allocation. Attacker controls both the overflow size and the
data written (file bytes at p_offset). Root-only (SYSCAP_NOKLD); the cleanest
heap-overflow primitive in the ELF loaders.
Root cause
case PT_LOAD: /* :507 no p_filesz <= p_memsz check */
segs[nsegs] = phdr;
...
mapsize = round_page(segs[1]->p_vaddr + segs[1]->p_memsz) - trunc_page(segs[0]->p_vaddr); /* :543 */
ef->address = kmalloc(mapsize, M_LINKER, M_WAITOK); /* :546 */
...
vn_rdwr(UIO_READ, vp, segbase, segs[i]->p_filesz, ...); /* :554 writes p_filesz */
bzero(segbase + segs[i]->p_filesz, segs[i]->p_memsz - segs[i]->p_filesz); /* :562 underflow */
Recommended fix
--- a/sys/kern/link_elf.c
+++ b/sys/kern/link_elf.c
@@ -507,6 +507,11 @@
case PT_LOAD:
+ if (phdr->p_filesz > phdr->p_memsz) {
+ link_elf_error("p_filesz > p_memsz");
+ error = ENOEXEC;
+ goto out;
+ }
if (nsegs == 2) {
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/link_elf.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0056 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_ko.py | trigger-source | builds ELF .ko with p_filesz>p_memsz | 4.3 KB | view raw |
| df0056_overflow.ko | trigger-source | prebuilt malicious module | 12.2 KB | β download |
| build.sh | build-script | run craft_ko.py | 550 B | view raw |
| run.sh | run-script | kldload ./df0056_overflow.ko (root) | 425 B | view raw |
| panic.txt | panic-signature | baseline page fault in memmove | 725 B | view raw |
| fix.diff | suggested-fix | reject p_filesz > p_memsz in PT_LOAD | 581 B | view raw |
| fix_run.log | run-log | patched kernel: ENOEXEC reject, guest up | 328 B | view raw |
| fix_dmesg.txt | dmesg | patched-kernel dmesg showing reject message | 839 B | view raw |
| env.txt | environment | uname, cc version | 232 B | view raw |
| VERDICT.md | verdict | full narrative + bright-line analysis | 2.6 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0056 β Heap overflow via unchecked p_filesz > p_memsz in ELF loader
Verdict: REPRODUCED (kernel heap overflow -> panic); root-only (defense-in-depth)
link_elf_load_file() (sys/kern/link_elf.c:388-...), the kernel-module
loader invoked by kldload(2), sizes its buffer from p_memsz but writes
p_filesz bytes into it β with no p_filesz <= p_memsz validation:
link_elf.c:541-543 base_vlimit = round_page(segs[1]->p_vaddr + segs[1]->p_memsz);
mapsize = base_vlimit - base_vaddr;
link_elf.c:546 ef->address = kmalloc(mapsize, M_LINKER, M_WAITOK); /* sized by memsz */
link_elf.c:554-556 vn_rdwr(UIO_READ, vp, segbase, segs[i]->p_filesz, ...); /* writes filesz */
link_elf.c:562-563 bzero(segbase + p_filesz, p_memsz - p_filesz); /* underflow! */
If p_filesz > p_memsz:
1. vn_rdwr writes p_filesz bytes into a buffer sized for p_memsz β
heap overflow of p_filesz - round_page(...p_memsz...) bytes;
2. bzero(segbase + p_filesz, p_memsz - p_filesz) β the length is negative,
wraps to a huge size_t β massive zero-fill past the buffer.
Reproduction (root only)
craft_ko.py builds a minimal ET_DYN ELF with a PT_LOAD segment where
p_memsz=0x100, p_filesz=0x2000. kldload as root:
baseline (#0): Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8011891f000 (supervisor WRITE, page not present)
Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)
The page fault is in memmove (the bcopy inside vn_rdwr) writing
p_filesz bytes past the mapsize-sized heap allocation β the overflow is
real and the bytes (0x41 from the file) are attacker-controlled.
Why this is NOT an unpriv->root escalation (bright-line rule)
sys_kldload (kern_linker.c:794) gates on caps_priv_check_self(SYSCAP_NOKLD)
β root only. There is no unprivileged path to this write; it is a
root->kernel hardening gap. The primitive (controlled heap overflow +
underflow bzero) is genuine, but the privilege boundary to cross (root ->
kernel) is already game-over, so it does not yield uid=0 from an
unprivileged user. Reported as panic/corruption for the default kernel.
Fix (validated on a built single-fix kernel)
fix.diff adds if (phdr->p_filesz > phdr->p_memsz) { ENOEXEC; goto out; }
in the PT_LOAD case, before the segment is recorded. Validated end-to-end:
patched (#1): kldload: p_filesz > p_memsz in PT_LOAD
KLD_RC=1, module NOT loaded, guest stays UP
vs the baseline panic above.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). link_elf p_filesz>p_memsz -> heap overflow. Root-only (SYSCAP_NOKLD).
No comments yet.