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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2771 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
DF-2771 β VERDICT
REPRODUCED (panic / uncontrolled kernel-memory write, root-gated).
Root cause (sys/kern/link_elf_obj.c)
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-641vm_object_allocate(..., round_page(mapsize) >> PAGE_SHIFT)β sized from the wrapped total (0x1010 β 2 pages);:663-667vm_map_find(kernel_map, ..., round_page(mapsize), ...)β 2-page wired mapping;:733-760per-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
fixedPatched 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'
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.
No comments yet.