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

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2808 Β· 11 files
FileTypeDescriptionSize
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)

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.

VERDICT.md
↓ download raw

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)

sys/kern/link_elf.c:541-543:

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:

  1. per-segment p_filesz <= p_memsz (completes DF-0056's missing check),
  2. per-segment p_vaddr + p_memsz wraparound rejection,
  3. segs[0]->p_vaddr + segs[0]->p_memsz <= segs[1]->p_vaddr + segs[1]->p_memsz (ordering + seg0-extent-inside-mapsize),
  4. mapsize != 0,
  5. nsegs == 2 before 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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']
↓ 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 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.