Cross-segment mapsize under-sizing: mapsize derives only from segs[0]'s start and segs[1]'s end, so segs[0]'s extent is never covered β file-controlled kernel heap overflow (DF-2771 analogue in the EXEC loader)
| Field | Value |
|---|---|
| ID | DF-2808 |
| 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 (CWE-190 ordering/wraparound variants) |
| File | sys/kern/link_elf.c |
| Lines | 541-543 (sizing), 552-563 (unbounded segment writes) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
link_elf_load_file sizes the module mapping as
round_page(segs[1]->p_vaddr + segs[1]->p_memsz) β
trunc_page(segs[0]->p_vaddr) and then reads p_filesz file bytes at
mapbase + p_vaddr β base_vaddr per segment. segs[0]'s own end is never
required to fit inside mapsize, ordering is unchecked, and
p_vaddr+p_memsz wraparound is unchecked β the "text then data, in that
order" comment is unenforced. A module with seg0 {vaddr 0, filesz=memsz
4MB} and seg1 {vaddr 0x100, filesz=memsz 0x100} gets a 1-page kmalloc
and a 4MB file-controlled write into it. Both segments satisfy
p_filesz β€ p_memsz β strictly more general than DF-0056.
Threat model & preconditions
Root-supplied crafted ET_EXEC .ko (SYSCAP_NOKLD-gated) β deterministic contiguous kernel-heap overflow with fully file-controlled content, length and offset chosen by the module author. Defeats module-content whitelisting/sandboxing defense-in-depth; memory outside the loader's allocation is modified before the load fails or completes.
Proof of contest
VERIFIED on the stock kernel (findings/poc/DF-2808/): Fatal trap 12
... supervisor write data ... Stopped at memmove+0x10a β ~64KB of
adjacent kernel heap overwritten with file bytes before the first
unmapped page. Patched kernel rejects with "Overlapping or
out-of-order segments" ENOEXEC. uid0-escalation N/A (kldload root-gated;
a working .ko is already arbitrary kernel code for that attacker).
Recommended fix
Per-segment validation (p_fileszβ€p_memsz, non-wrap), ordering requirement, nsegs==2, mapsize!=0 β full validated fix.diff in the pack.
Timeline
- 2026-08-31 Discovered during pass-2 audit of link_elf.c (GLM 5.3); stock-kernel overflow reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2808 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| gen_exec_ko.py | β | 6.3 KB | view raw | |
| build.sh | β | 122 B | view raw | |
| run.sh | β | 351 B | view raw | |
| run.log | β | 770 B | view raw | |
| panic.txt | β | 2.0 KB | view raw | |
| env.txt | β | 512 B | view raw | |
| fix.diff | β | 4.1 KB | view raw | |
| run_fix.log | β | 1.4 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download |
DF-2808 β Cross-segment mapsize under-sizing β file-controlled kernel heap overflow in link_elf_load_file
Build
python3 gen_exec_ko.py seg-overflow.ko seg-overflow
(host-side; no guest toolchain needed. Produces a 4.2 MB ET_EXEC x86-64 module.)
Run
scp -F dfbsd-qemu/config seg-overflow.ko dfbsd:/tmp/ dfbsd-qemu/vm.sh run_root 'kldload /tmp/seg-overflow.ko; echo RC=$?' dfbsd-qemu/vm.sh log 30 # panic signature
Expected (stock kernel, verified 2026-09-01)
Fatal trap 12: page fault while in kernel mode fault code = supervisor write data, page not present Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)
Guest panics (write fault inside the vn_rdwr/uiomove copy). Before the fault, ~64 KB of adjacent kernel heap has already been overwritten with 0x41 bytes read from the module file.
Expected (patched kernel, fix.diff applied)
kldload: Segment ... error, RC != 0, guest stays up
(specifically "kldload: Overlapping or out-of-order segments" + ENOEXEC)
Why (root cause, sys/kern/link_elf.c)
link_elf_load_file sizes the module mapping ONLY from the first segment's
start and the SECOND segment's end:
541: base_vaddr = trunc_page(segs[0]->p_vaddr);
542: base_vlimit = round_page(segs[1]->p_vaddr + segs[1]->p_memsz);
543: mapsize = base_vlimit - base_vaddr;
546: ef->address = kmalloc(mapsize, M_LINKER, M_WAITOK);
553: segbase = mapbase + segs[i]->p_vaddr - base_vaddr;
554: vn_rdwr(UIO_READ, vp, segbase, segs[i]->p_filesz, ...)
segs[0]'s own extent (p_vaddr + p_memsz, and p_filesz) is never required
to fit inside mapsize, and the "text then data, in that order" assumption
(line 497 comment) is never enforced. The trigger module uses
segs[0]: p_vaddr=0, p_filesz=p_memsz=0x400000 (4 MB of 'A') segs[1]: p_vaddr=0x100, p_filesz=p_memsz=0x100 => mapsize = round_page(0x200) - 0 = 0x1000 (one page) => vn_rdwr writes 4 MB of file-controlled bytes into the 1-page kmalloc
Even with p_filesz <= p_memsz in BOTH segments (so DF-0056's check-site is
not involved), the read overruns the allocation. Out-of-order segments
(seg1 below seg0) underflow mapsize to ~2^64; p_vaddr + p_memsz wrapping
2^64 shrinks base_vlimit β same unvalidated-arithmetic family, all fixed by
the segment-layout validation in fix.diff.
Threat model: kldload requires root (SYSCAP_NOKLD) β root-supplied-input class, consistent with DF-0056 (Medium). The primitive is a deterministic, fully file-controlled heap overflow (write-what-where contiguous) in the kernel M_LINKER heap.
DF-2808 VERDICT β reproduced (panic / kernel heap overflow, root-gated)
Status: reproduced. Verified on the 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).
How it manifests
kldload /tmp/seg-overflow.ko (as root) panics the guest:
Fatal trap 12: page fault while in kernel mode cpuid = 3; lapic id = 3 fault virtual address = 0xfffff80116e69000 fault code = supervisor write data, page not present <-- WRITE Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)
(run.log, panic.txt). The fault is a supervisor WRITE inside the
memmove that vn_rdwr/uiomove uses to copy file bytes into the module
mapping β i.e. the overflow ran off the end of the mapping while still
copying attacker file data. Everything between the end of the 1-page
kmalloc (0x1000 bytes, M_LINKER) and the faulting page
(0xfffff80116e69000, β64 KB later) was overwritten with 0x41 bytes
read from the module file before the first unmapped page stopped the copy.
Root cause (path:line)
base_vaddr = trunc_page(segs[0]->p_vaddr);
base_vlimit = round_page(segs[1]->p_vaddr + segs[1]->p_memsz);
mapsize = base_vlimit - base_vaddr;
The mapping size accounts only for segs[0]'s start and segs[1]'s end.
segs[0]'s own end (p_vaddr + p_memsz) is never checked against
mapsize, nor is segment ordering, nor 64-bit wraparound of
p_vaddr + p_memsz. sys/kern/link_elf.c:552-564 then reads
p_filesz file bytes at mapbase + p_vaddr - base_vaddr and bzeros
p_memsz - p_filesz β both unbounded by the actual allocation when the
arithmetic lies.
The comment at sys/kern/link_elf.c:496-499 documents the assumption
("We rely on there being exactly two load segments, text and data, in that
order") but nothing enforces it. This is the DF-2771 analogue
(mapsize accumulation overflow in link_elf_obj.c) expressed through THIS
file's distinct program-header-driven code path, and it is strictly more
general than DF-0056 (which needs p_filesz > p_memsz within one segment):
here both segments satisfy p_filesz <= p_memsz and the overflow still
happens because the allocation is sized from the wrong expression.
Exploit chain (characterization)
Primitive: contiguous kernel-heap overflow with 100% file-controlled content, overflow length and start offset chosen by the module author (segs[0].p_filesz up to file size, segs[1] placement controlling mapsize). Consumers of adjacent M_LINKER/slab memory at the moment of load include previously-loaded module mappings and linker_file structures.
Honest ceiling: kldload is root-gated (SYSCAP_NOKLD,
sys/kern/kern_linker.c kldload syscall), so the attacker model is
"malicious root-supplied module file" β the same class as DF-0056. For
that attacker, a working .ko is already arbitrary kernel code; the bug's
operational impact is (a) kernel memory corruption/panic from a file that
should have been rejected with ENOEXEC, and (b) it defeats any
defense-in-depth scheme that sandboxes or whitelists module content
(e.g. signed-hash whitelisting of known modules) because memory outside
the loader's allocation is modified. No unprivilegedβroot chain exists
through this path (verified: kldload requires uid 0 / kern.securelevel
checks).
Fix validation
fix.diff (this pack) adds, before the mapping is sized:
- per-segment
p_filesz <= p_memsz(completes DF-0056's missing check), - per-segment
p_vaddr + p_memszwraparound rejection, segs[0]->p_vaddr + segs[0]->p_memsz <= segs[1]->p_vaddr + segs[1]->p_memsz(ordering + seg0-extent-inside-mapsize),mapsize != 0,nsegs == 2before any segs[] dereference.
Built as make -j6 nativekernel KERNCONF=X86_64_GENERIC in the guest,
installed, rebooted. Re-run of the exact PoC: rejected with
kldload: Overlapping or out-of-order segments, RC!=0, guest stays up,
zero panics. See fix_build.log / run_fix.log.
Fix verification
fixedPatched kernel #1 rejects the module with 'kldload: Overlapping or out-of-order segments' (ENOEXEC); no write fault, no panic, guest stays up (run_fix.log). Control module globalsym.ko still loads/unloads cleanly (no regression).
['run_fix.log', 'fix_build.log']
Confirmed kernel references
Detail
Exploit chain
crafted ET_EXEC .ko -> kldload(root) -> mapsize=round_page(vaddr1+memsz1)-trunc(vaddr0) ignores segs[0] extent -> kmalloc(mapsize) -> vn_rdwr copies p_filesz0 file-controlled bytes at mapbase -> contiguous heap overflow of M_LINKER allocation (observed ~64KB of 0x41 written past a 4KB chunk before the first unmapped page faulted). uid0 chain N/A: kldload is root-gated; for the root attacker a valid .ko is already kernel code.
Evidence (decisive lines)
["run.log: 'fault code = supervisor write data, page not present' + 'Stopped at memmove+0x10a: repe movsq'", 'panic.txt: full Fatal trap 12 dump, current process 835 (kldload)', "run_fix.log: 'kldload: Overlapping or out-of-order segments', RC=1, uptime shows guest never went down on patched kernel", 'fix_build.log: patched nativekernel build confirmation']
PoC changes
authored fresh this pass: ET_EXEC/program-header module generator (gen_exec_ko.py, adapted from the DF-2771 ET_REL pattern)
Verified recommended fix
validate per-segment p_filesz<=p_memsz and p_vaddr+p_memsz non-wrap, require segs[0] end <= segs[1] end, reject mapsize==0 and nsegs!=2 before sizing/allocating the mapping (fix.diff)
Verdict
Deterministic file-controlled kernel heap overflow reproduced on the stock guest: mapsize derives only from segs[0].p_vaddr's start and segs[1]'s end (link_elf.c:541-543), so a module whose first PT_LOAD extends beyond the second segment's end gets a mapping far smaller than the per-segment vn_rdwr/bzero writes (link_elf.c:552-563). kldload of a 4MB segs[0] against a 1-page mapsize faulted with 'supervisor write data, page not present' in memmove after smearing ~64KB of adjacent kernel heap with file bytes. Root-gated (SYSCAP_NOKLD) so honest ceiling is Medium, same class as DF-0056 but needs no p_filesz>p_memsz; ordering/wraparound variants underflow mapsize to ~2^64 (kmalloc huge) or below-buffer writes. Fixed and validated by the segment-layout validation in fix.diff.
No comments yet.