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)
PoC verification
Evidence pack
findings/poc/DF-3240 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 addresssbuf_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_MAXfalse); 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_testableObservable (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
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.
No comments yet.