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

Signed-overflow bypass of the buflen guard in procfs_domap(): wrapped sum survives the INT_MAX check, int-truncated sbuf, wild fault-protected kernel-side read

Summary

buflen = uio_offset + uio_resid (:61) is only tested 'buflen >= INT_MAX' (:75); with uio_offset near 2^63 (pread) the ssize_t sum wraps negative and passes. sbuf_new truncates to int giving a tiny buffer; when the map text overflows it, sbuf_printf returns -1 and sbuf_finish returns ENOMEM so buflen is NOT replaced by sbuf_len (:243-244) - it keeps the wrapped value and uiomove_frombuf executes uiomove(heap_kva + ~2^63, resid, uio). REPRODUCED unpriv on stock kernel: 128 protection-alternating anon mappings (prevent entry merging), pread at 0x7ffffffffffffff8 -> 'pread -> n=-1 errno=14 (Bad address)' - EFAULT only producible by uiomove faulting on the wild source. Impact ceiling genuinely none beyond guard bypass (wrapped address always lands in user range; onfault window prevents panic; attacker pages echo their own bytes) - also proves DF-0924's INT_MAX cap is bypassable. Fix: overflow-safe bound (uio_offset > INT_MAX - uio_resid).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3240 Β· 8 files
FileTypeDescriptionSize
df3240_buflen_overflow.c β€” 3.4 KB view raw
build.sh β€” 118 B view raw
build.log β€” 13 B view raw
run.log β€” 106 B view raw
env.txt β€” 187 B view raw
fix.diff β€” 764 B view raw
VERDICT.md β€” 2.3 KB ↓ raw
verdict.json β€” 2.9 KB view raw
VERDICT.md
↓ download raw

DF-3240 VERDICT β€” REPRODUCED (guard bypass + wild fault-protected read; impact: none)

What was run

df3240_buflen_overflow.c (uid 1001 maxx) on stock kernel #0: 1. plain read of /proc/self/map -> 1416 bytes (sanity); 2. 128 protection-alternating anon mmaps inflate the map (prevents entry merging) so the truncated sbuf overflows; 3. pread(fd, buf, 0x1000, 0x7ffffffffffffff8ULL).

Result (run.log)

plain map size = 1416 bytes
pread -> n=-1 errno=14 (Bad address)
done, still alive (kernel did not panic)

Interpretation

  • A bounds-respecting kernel returns 0 (EOF) or EINVAL for this pread; EFAULT can only come from uiomove() faulting on the wild SOURCE address sbuf_data(sb) + 0x7ffffffffffffff8 (heap_kva + 2^63, wrapping into the top of user VA, unmapped). std_copyout's PCB_ONFAULT window (sys/platform/pc64/x86_64/support.s:228-279) catches the fault -> EFAULT instead of a panic.
  • Math: buflen = 0x7ffffffffffffff8 + 0x1000 = 0x8000000000000FFE (negative ssize_t) passes procfs_map.c:75 (buflen >= INT_MAX false); sbuf_new int-truncates buflen+1 to 0xFFF (4095-byte buffer); overflow path leaves buflen wrapped (sbuf_finish != 0 at procfs_map.c:243); uiomove_frombuf (kern_subr.c:272-282) passes the roundtrip offset check (0x7fff... < 2^63) and copies from the wild address.
  • Impact ceiling: no kernel disclosure (wrapped address is always user range: kernel heap >= 0xffff800000000000 minus ~2^63 lands <= 0x7fff_ffff_ffff_ffff), no panic (onfault), no DoS. Demonstrates the guard bypass concretely and that DF-0924's INT_MAX alloc cap is bypassable. Severity Low (hardening).

Fix

Overflow-safe bound check (fix.diff): reject when uio_offset > INT_MAX - uio_resid (both already checked non-negative). fix_status=not_testable: the observable (EFAULT) is benign; validating "EFAULT becomes EINVAL" requires a second kernel build which is not warranted for a Low/hardening finding with impact none.

Artifacts

  • run.log (decisive), build.log, env.txt, fix.diff

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Observable (EFAULT) is benign by construction; a second kernel build to watch EFAULT become EINVAL is not warranted for a Low/hardening finding β€” fix.diff is line-accurate against the cited guard.

fix.diff; run.log
↓ fix.diffper-fix-DF-3240

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 'plain map size = 1416 bytes' then 'pread -> n=-1 errno=14 (Bad address)' β€” EFAULT can only originate from uiomove faulting on the wild source address; a bounds-respecting path returns 0/EINVAL", 'VERDICT.md: full arithmetic (buflen=0x8000000000000FFE, int-truncated length 0xFFF) and why KASSERT-negative-length is mathematically unreachable']

PoC changes

Initial draft (from the finding sketch) returned n=0: the 128 identical adjacent anon mappings merged into one entry so the truncated sbuf never overflowed. Fixed by alternating protections per mapping and adding a plain-read sanity print of the map size.

Verified recommended fix

Overflow-safe bound: reject when uio_offset > INT_MAX - uio_resid (both already non-negative) instead of testing the wrapped sum (see fix.diff).

Verdict

Guard bypass reproduced as predicted, impact ceiling genuinely none. pread(fd,buf,0x1000,0x7ffffffffffffff8) on /proc/self/map makes buflen=uio_offset+uio_resid wrap negative past the `buflen >= INT_MAX' test (procfs_map.c:61,75); sbuf_new truncates buflen+1 to int (4095-byte buffer); the inflated map overflows the sbuf so sbuf_finish returns ENOMEM and buflen keeps the wrapped 0x8000000000000FFE; uiomove_frombuf then performs uiomove(heap_kva + 0x7ffffffffffffff8, ...) -- a wild kernel-side read that wraps into the top of USER VA. std_copyout's onfault catches the fault: pread returns EFAULT (observed) or echoes the attacker's own mapped bytes. No kernel disclosure possible (wrapped address always < 2^63), no panic (fault-protected), no DoS. Filed Low/hardening; also documents that DF-0924's INT_MAX allocation cap is bypassable via the same wrap.