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

mapsize 64-bit accumulation overflow in link_elf_obj_load_file sizes the KVA mapping below the per-section bzero/read lengths β†’ wild zero-fill past the mapping

Field Value
ID DF-2771
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H
CWE CWE-190 β†’ CWE-787
File sys/kern/link_elf_obj.c
Lines 621-633 (sizing), 640-667 (mapping), 734-760 (load)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The sizing loop adds each PROGBITS/NOBITS sh_size into size_t mapsize with no overflow check. Two NOBITS sections sized 0xFFFFFFFFFFFFF000 and 0x2000 (align 1) wrap the total to 0x1010, so vm_object_allocate/ vm_map_find create a 2-page wired mapping, after which the load loop executes bzero(mapbase+16, 2^64-0x1000) β€” an uncontrolled kernel-memory zero-fill running until the first unmapped page. The consistency panic later in the load is unreachable β€” the wild bzero runs first. Root-supplied-input class (SYSCAP_NOKLD); no unpriv reach, no leak, no execution control β€” honest ceiling is kernel-memory wipe + panic (if a live mapping sat directly above the module its contents would be blind-zeroed first).

Proof of concept

VERIFIED on the stock guest (findings/poc/DF-2771/): crafted .ko with the wrapping NOBITS pair β†’ Fatal trap 12: page fault, supervisor write data stopped at memset+0xd5 (repe stosq), fault VA = first page past the mapping. Patched kernel: clean ENOEXEC ("section too large"), guest survives. No uid0 route (trigger requires root; write uncontrolled).

Cap sh_size in the sizing loop (LINKER_MAX_SECSIZE = 1LL << 44) so the u64 accumulator cannot wrap β€” validated fix.diff in the pack.

Timeline

  • 2026-08-30 Discovered during pass-2 audit of link_elf_obj.c (GLM 5.3); panic reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2771 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
gen_module.py β€” 3.7 KB view raw
build.sh β€” 115 B view raw
run.sh β€” 285 B view raw
run.log β€” 1.3 KB view raw
panic.txt β€” 665 B view raw
build.log β€” 5.7 MB ↓ download
env.txt β€” 191 B view raw
fix.diff β€” 5.6 KB view raw
verdict.json β€” 4.6 KB view raw

DF-2771 β€” mapsize 64-bit wrap in link_elf_obj_load_file β†’ undersized KVA mapping β†’ wild bzero past its end

Build (host)

python3 gen_module.py df2771.ko mapsize-wrap

(no guest build needed β€” the trigger is a hand-crafted 554-byte ET_REL object)

Run (guest, root β€” kldload is SYSCAP_NOKLD-gated)

scp -F dfbsd-qemu/config df2771.ko dfbsd:/tmp/
dfbsd-qemu/vm.sh run_root 'kldload /tmp/df2771.ko'

Expected β€” stock kernel (baseline, reproduced)

Serial console (see panic.txt):

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff82602000
fault code = supervisor write data, page not present
Stopped at memset+0xd5: repe stosq

The two NOBITS sections declare sh_size 0xFFFFFFFFFFFFF000 and 0x2000 (sum ≑ 0x1010 mod 2^64), so the mapsize accumulator wraps to 0x1010 β†’ a 2-page mapping is created, then bzero(mapbase+16, 2^64-0x1000) (link_elf_obj.c:760) zero-fills forward until the first unmapped page past the mapping β†’ panic.

Expected β€” patched kernel (fix.diff applied, kernel #1 Sep 1 2026)

kldload: /tmp/df2771.ko: section too large

RC=1, no panic, guest stays up (ALL-SURVIVED).

Threat model

kldload(2) is root-only (caps_priv_check_self(SYSCAP_NOKLD), kern_linker.c:792) and blocked at securelevel>0 β€” this is root-supplied-input / verified-boot class (malformed or substituted module), consistent with the Low-severity calibration of DF-0040/0041/0042.

VERDICT.md
↓ download raw

DF-2771 β€” VERDICT

REPRODUCED (panic / uncontrolled kernel-memory write, root-gated).

The sizing loop at sys/kern/link_elf_obj.c:621-633 accumulates every SHT_PROGBITS/SHT_NOBITS sh_size into a plain size_t mapsize with no overflow check and no per-section cap:

621:    for (i = 0; i < hdr->e_shnum; i++) {
622:        if (shdr[i].sh_size == 0)
623:            continue;
624:        switch (shdr[i].sh_type) {
625:        case SHT_PROGBITS:
626:        case SHT_NOBITS:
627:            alignmask = shdr[i].sh_addralign - 1;
628:            mapsize += alignmask;
629:            mapsize &= ~alignmask;
630:            mapsize += shdr[i].sh_size;      /* <-- u64 wrap */

sh_size is an attacker-chosen Elf64_Xword taken verbatim from the module's section headers. With e_shnum ≀ 65535 sections, any total β‰₯ 2^64 requires at least one section β‰₯ 2^48, so a wrap necessarily leaves a monster section whose load-side length is used raw:

  • :640-641 vm_object_allocate(..., round_page(mapsize) >> PAGE_SHIFT) β€” sized from the wrapped total (0x1010 β†’ 2 pages);
  • :663-667 vm_map_find(kernel_map, ..., round_page(mapsize), ...) β€” 2-page wired mapping;
  • :733-760 per-section load: NOBITS β†’ bzero(ef->progtab[pb].addr, shdr[i].sh_size) with the raw 2^64-0x1000 length.

The consistency panic at :812-815 cannot save this β€” the wild bzero runs first (and the placement arithmetic itself stays self-consistent under wrap).

Sibling unchecked consumers of the same field on the same path: :734-738 (vn_rdwr into the mapping for PROGBITS β€” file-backed, so not practical past EOF) and the alignment truncation alignmask = sh_addralign - 1 (int), which was analyzed and found to either stay within the mapping or land on mapbase == 0 β†’ caught by the NULL check at :727-730 (ENOSPC).

Reproduction (baseline, stock kernel)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 … X86_64_GENERIC x86_64, securelevel -1. Trigger module built by gen_module.py df2771.ko mapsize-wrap (host python3; no guest toolchain needed): 7 sections β€” NULL, .text(16B PROGBITS), .symtab(1 sym), .strtab, .shstrtab, NOBITS sh_size=0xFFFFFFFFFFFFF000 align=1, NOBITS sh_size=0x2000 align=1.

kldload /tmp/df2771.ko β†’ ssh session hangs; serial console:

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff82602000     <- first page past the 2-page mapping
fault code = supervisor write data, page not present
Stopped at memset+0xd5: repe stosq             <- the wild bzero

(panic.txt, full excerpt of the decisive run.)

Primitive characterization (why impact stops at panic)

The write is a zero-fill of unbounded length starting inside the module's own 2-page mapping, running forward until the first unmapped page. The attacker controls start (mapbase+section offsets, sub-page granularity) but neither the written value (always 0x00) nor a stop point: the kernel_map region above a freshly-allocated module mapping is unmapped in practice (proven by the immediate fault at the mapping edge, VA 0xffffffff82602000), so the deterministic outcome is a fatal supervisor-write page fault. Had a live mapping been adjacent above the module, its contents would be zeroed first (blind corruption), but the run still terminates in the same fault β€” there is no route from this primitive to controlled execution, and the triggering user is already root (kldload is SYSCAP_NOKLD-gated, kern_linker.c:792, and blocked at securelevel>0, kern_linker.c:789). No leak and no unprivileged reach β€” uid0 escalation is out of scope for this class (the loader input is root-supplied), so the run is honestly closed at impact=panic.

Fix validation

fix.diff (this pack; identical in DF-2771/2772/2773 packs β€” one combined patch): adds LINKER_MAX_SECSIZE (1LL<<44) cap checked in the sizing loop β†’ ENOEXEC "section too large" before any allocation. With e_shnum ≀ 65535 the accumulator can then never exceed ~2^60.3, so wrap is impossible.

Applied to the guest's /usr/src copy (sys/ tree untouched in the repo), make -j6 nativekernel KERNCONF=X86_64_GENERIC (build.log, 37903 lines, -Werror clean, --- link_elf_obj.o ---), make installkernel, reboot into #1: Tue Sep 1 01:04:57 UTC 2026. Re-run of the exact same PoC:

kldload: /tmp/df2771.ko: section too large    (dmesg)
RC=1, guest stays up, 0 panics in console log

Baseline panic gone on the patched kernel β†’ fix_status=fixed.

Negative space checked on the same path (no findings)

  • nbytes = e_shnum * e_shentsize (:497): e_shentsize==64 is enforced before kmalloc; max 65535*64 fits int.
  • Huge-but-unwrapped mapsize: fails cleanly in vm_map_find β†’ ENOMEM.
  • sh_addralign==0 / 2^32 truncation: mapbase clamps to 0 β†’ caught by the NULL-addr check at :727-730 (ENOSPC).
  • vm_object refcount handling across wiring failure/unload (:646-673, :900-910): correct.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel rejects the identical module with 'kldload: /tmp/df2771.ko: section too large' (ENOEXEC, RC=1), no panic, guest stays up; baseline kernel #0 panics on the same input. fix.diff authored against read-only sys/ and applied only inside the guest's /usr/src copy.

run.log (baseline vs patched), build.log (kernel #1 build), dmesg line 'kldload: /tmp/df2771.ko: section too large'
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 01:04:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root crafts 554-byte ET_REL module (gen_module.py mode=mapsize-wrap) -> kldload -> link_elf_obj_load_file sizes mapping at wrapped 0x1010 -> bzero of 2^64-0x1000 bytes at the 2-page mapping -> supervisor-write page fault -> kernel panic. No uid0 route: uncontrolled zero-fill only, and the input channel requires root (SYSCAP_NOKLD), so escalation is out of scope for this class.

Evidence (decisive lines)

['panic.txt β€” baseline serial-console excerpt: Fatal trap 12, supervisor write data page not present, stopped at memset+0xd5 repe stosq, fault VA 0xffffffff82602000 (= mapping end + 0)', "run.log β€” baseline (panic, ssh hang) vs patched ('section too large', RC=1, guest up) transcripts", 'gen_module.py β€” trigger generator (mode mapsize-wrap); shdr[5] NOBITS size=0xfffffffffffff000, shdr[6] NOBITS size=0x2000, both align=1', 'build.log β€” full 37903-line untrimmed nativekernel build of the fix (link_elf_obj.o compiled -Werror clean)', 'VERDICT.md β€” full path:line trace, primitive characterization, negative space (alignmask truncation, nbytes int math, unwrapped-huge case)', 'fix.diff β€” LINKER_MAX_SECSIZE cap validated by baseline-vs-patched rerun']

PoC changes

n/a β€” trigger authored fresh for this finding (hand-crafted ET_REL, no prior seed); generator supports three modes, this pack uses mapsize-wrap

Verified recommended fix

Cap sh_size in the mapsize sizing loop (LINKER_MAX_SECSIZE = 1<<44) and reject the module with ENOEXEC; keeps the u64 accumulator from wrapping so the mapping always covers every per-section bzero/read length.

Verdict

mapsize accumulation in link_elf_obj_load_file (sys/kern/link_elf_obj.c:621-633) has no overflow check on attacker-chosen sh_size values; two NOBITS sections sized 0xFFFFFFFFFFFFF000 and 0x2000 wrap the 64-bit total to 0x1010, so only a 2-page KVA mapping is allocated (:640-667) while the load loop then executes bzero(mapbase+16, 2^64-0x1000) (:760) β€” an uncontrolled zero-fill that runs past the mapping until the first unmapped page. Reproduced on the stock guest as 'Fatal trap 12: supervisor write data, page not present' stopped at memset+0xd5 (repe stosq), fault VA = first page past the mapping. The write value (zero) and stop point are not controllable, so the realistic ceiling is kernel-memory wipe of anything mapped above the module followed by certain panic; trigger is root-supplied (kldload is SYSCAP_NOKLD-gated), so no unprivileged reach β€” impact honestly recorded as panic. fix.diff caps sh_size at LINKER_MAX_SECSIZE (1<<44) in the sizing loop; validated on a rebuilt kernel (#1 Sep 1 2026): same PoC now fails cleanly with 'kldload: section too large', no panic, guest survives.