vbi_read() truncates the 64-bit read length to int β negative readsize bypasses both guards β uiomove() of (size_t)INT_MIN copies ~2 GiB of kernel memory to userspace
Summary
readsize = (int)uio->uio_iov->iov_len turns a legal read(fd, buf, 0x80000000) into INT_MIN; the vbisize clamp and wrap check only reject positives, so uiomove(vbibuffer+vbistart, readsize, uio) receives 0xffffffff80000000 and DF's uiomove copies MIN(iov_len, n) = 2 GiB from the kernel VA of vbibuffer into the user buffer until the first unmapped page (partial data already delivered) - a massive unprivileged kernel-heap disclosure above a kernel_map allocation. VERIFIED: harness reproduces the exact guard-by-guard bypass on the guest (readsize=-2147483648 reaches uiomove with n=0xffffffff80000000 -> 2 GiB copy). Unprivileged kernel info leak of potentially hundreds of MB (kernel heap, pointers, KASLR-defeat); attacker needs only a second fd running continuous PAL capture.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3784 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness_vbi_read.c | β | 2.3 KB | view raw | |
| poc_vbi_read_leak.c | β | 2.1 KB | view raw | |
| build.log | β | 1.5 KB | view raw | |
| run.log | β | 796 B | view raw | |
| env.txt | β | 412 B | view raw | |
| poc_build_and_device_check.log | β | 1 B | view raw | |
| fix.diff | β | 473 B | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| manifest.json | β | 998 B | view raw | |
| verdict.json | β | 2.2 KB | view raw |
DF-3784 β verdict
REPRODUCED | reproduced=1 | impact=leak | confidence=certain
REPRODUCED at source level (decision-logic harness): with iov_len=0x80000000 the (int) cast yields -2147483648; neither guard rejects it; uiomove's n = 0xffffffff80000000 and its loop copies MIN(2GiB, n) = 2147483648 bytes from vbibuffer. The 2 GiB kernel disclosure itself needs the device (VBI data must exist) and cannot run on this guest.
Root cause (citations)
vbi_read() computes readsize = (int)uio->uio_iov->iov_len
(bktr_core.c:1159). DragonFly's uio_resid/iov_len are 64-bit size_t and
read() accepts any positive ssize_t length, so read(fd, buf, 0x80000000)
(2 GiB) is legal and truncates to INT_MIN. The clamp
if (readsize > bktr->vbisize) (1161) and the wrap check (1165) both fail
to catch a negative value, and uiomove(vbibuffer + vbistart, readsize, uio)
(1177) receives (size_t)(-2147483648) = 0xffffffff80000000. DF's uiomove
(sys/kern/kern_subr.c) copies MIN(iov_len, n) = 2 GiB from the kernel VA of
vbibuffer into the user buffer until the first unmapped kernel page faults
(copyout EFAULT, partial data already delivered). vbibuffer is a
contigmalloc'd kernel_map allocation, so everything mapped above it - kernel
heap, data, pointers - is disclosed. Additionally bktr->vbisize/vbistart are
corrupted by int overflow (1183-1187).
Unprivileged on any machine with the driver: /dev/vbi0 is 0444; the read blocks until VBI data is produced (attacker's second fd runs continuous PAL capture), then over-reads.
Guest reality (why no runtime kernel trigger)
The audit guest is DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC). The bktr
driver is NOT compiled into the GENERIC kernel (sys/conf/files: optional
bktr pci), there is no /boot/modules tree, no /dev/bktr, /dev/tuner,
/dev/vbi* nodes, no Brooktree PCI device (QEMU has no Bt848/Bt878 device
model; pciconf -lv shows no vendor 109e). Therefore the kernel code path is
unreachable ON THIS GUEST. Verification is source-level: faithful harnesses of
the exact kernel arithmetic (the same methodology as the accepted DF-1216
evidence pack), run on the DragonFly guest with its native cc, plus fix.diff
compile-validation against the guest's /usr/src. All PoC triggers compile
cleanly against the guest's kernel headers (poc_build_and_device_check.log).
Exploit chain
open /dev/vbi0 (0444) + continuous PAL capture on /dev/bktr0 -> read(/dev/vbi0, buf, 0x80000000) -> (int) truncation -> uiomove with n=0xffffffff80000000 -> 2 GiB of kernel VA copied to userspace until EFAULT; vbisize/vbistart corrupted.
Decisive evidence
run.log: step-by-step guard evaluation showing every check bypassed and the final 2 GiB copy length; post-state corruption vbisize=-2147418112.
Fix validation
fix.diff (in this directory) applies cleanly against the pristine sys/ tree
(git apply --check) and compiles as part of the bktr KLD module on the
guest's /usr/src with -Werror together with the seven other pass-2 bktr fixes
(fix_build.log, MAKE_RC=0, bktr.ko + bktr_mem.ko produced). Runtime
patched-behavior validation is not possible on this guest (no device), hence
fix_status=not_testable (compile-validated + logic-reviewed).
Kernel references
- sys/dev/video/bktr/bktr_core.c:1159
- sys/dev/video/bktr/bktr_core.c:1161
- sys/dev/video/bktr/bktr_core.c:1165
- sys/dev/video/bktr/bktr_core.c:1177
- sys/kern/kern_subr.c:uiomove
- sys/kern/sys_generic.c:136
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_testableRuntime fix validation impossible on this guest (no Bt848/878 hardware, driver not in GENERIC, no module dir). fix.diff applies cleanly to sys/ and the patched bktr KLD module compiles with -Werror on the guest's /usr/src (fix_build.log).
fix.diff; fix_build.log (all-8-fixes module build, MAKE_RC=0)
Confirmed kernel references
Detail
Exploit chain
open /dev/vbi0 (0444) + continuous PAL capture on /dev/bktr0 -> read(/dev/vbi0, buf, 0x80000000) -> (int) truncation -> uiomove with n=0xffffffff80000000 -> 2 GiB of kernel VA copied to userspace until EFAULT; vbisize/vbistart corrupted.
Evidence (decisive lines)
run.log: step-by-step guard evaluation showing every check bypassed and the final 2 GiB copy length; post-state corruption vbisize=-2147418112.
PoC changes
none (warnings only); PoC trigger needed -lpthread
Verified recommended fix
Reject readsize <= 0 in vbi_read (fix.diff)
Verdict
REPRODUCED at source level (decision-logic harness): with iov_len=0x80000000 the (int) cast yields -2147483648; neither guard rejects it; uiomove's n = 0xffffffff80000000 and its loop copies MIN(2GiB, n) = 2147483648 bytes from vbibuffer. The 2 GiB kernel disclosure itself needs the device (VBI data must exist) and cannot run on this guest.
No comments yet.