Missing ab_busycnt validation in hpfs_hpbmap allows OOB heap read via crafted fnode/alsec
Summary
hpfs_alsubr.c:80 for(i=0;i<abp->ab_busycnt;i++,anp++) walks alnode array. :114 for(i=0;i<abp->ab_busycnt;i++,alp++) walks alleaf array. ab_busycnt from on-disk alblk_t never validated against container size. fnode fn_abd[0x60]=96B alsec as_abd[0x1E0]=480B. Legitimate max busycnt: fnode-leaves 8 fnode-nodes 12 alsec-leaves 40 alsec-nodes 60. Set busycnt=255 loop reads 2040-3060 bytes past data area crossing heap object boundaries. fnode in kmalloc hpfsnode (vfsops.c:488) OOB crosses heap. Reachable: any user reading/stat a file on mounted crafted HPFS. info leak via returned bn=disk offset or panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0857 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | finding summary, build/run, expected vs fixed behaviour | 4.4 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, trigger, exploit-chain ceiling, fix validation | 10.2 KB | β raw |
| harness.c | trigger-source | deterministic OOB-proof harness: faithful loop transcription + poisoned allocator + fixed-mode control | 11.7 KB | view raw |
| craft_img.py | trigger-source | HPFS image crafter: SuperBlock/SpareBlock/bitmap/dirblk/fnode with forged ab_busycnt=255 | 9.4 KB | view raw |
| df857.img | trigger-source | crafted 64 KB HPFS image with forged busycnt=255 (binary, for live mount test) | 64.0 KB | β download |
| build.sh | build-script | exact cc command to build the harness | 159 B | view raw |
| run.sh | run-script | exact harness invocation | 135 B | view raw |
| build.log | build-log | full compiler output of the final successful harness build | 65 B | view raw |
| run.log | run-log | decisive harness run (full output) showing OOB extent + fix rejection | 1.2 KB | view raw |
| run.1.log | run-log | stress-test run 1 of 3 (leak/variance) | 1.2 KB | view raw |
| run.2.log | run-log | stress-test run 2 of 3 | 1.2 KB | view raw |
| run.3.log | run-log | stress-test run 3 of 3 | 1.2 KB | view raw |
| panic.txt | panic-signature | kernel panic signature from boot.log: panic: bgetvp - overlapping buffer via hpfs_read (live trigger) | 1.8 KB | view raw |
| env.txt | environment | uname, cc version, sysctls (vm.randomize_mmap=0, vfs.usermount=0) | 324 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: validate ab_busycnt against container max (fnode-leaves 8/nodes 12, alsec-leaves 40/nodes 60) before the loops at hpfs_alsubr.c:80/:114 | 2.1 KB | view raw |
| fix_build.log | build-log | full build output of the single-fix hpfs.ko module (Phase 8) | 10.2 KB | view raw |
| fix_run.log | run-log | patched-module re-run of cat /mnt/FILE: EINVAL + 'forged ab_busycnt 255 > max 8', guest stays up | 456 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0857 β Missing ab_busycnt validation in hpfs_hpbmap loops (OOB heap read via crafted HPFS alblk_t)
Cited path
sys/vfs/hpfs/hpfs_alsubr.c:80 (alnode loop, AB_NODES branch) sys/vfs/hpfs/hpfs_alsubr.c:114 (alleaf loop, AB_LEAF branch) sys/vfs/hpfs/hpfs.h:176 (ab_busycnt: u_int8_t, attacker-controlled) sys/vfs/hpfs/hpfs.h:172-178 (alblk_t 8-byte container header) sys/vfs/hpfs/hpfs.h:200 (fn_abd[0x60] = 96-byte fnode alloc-block data area) sys/vfs/hpfs/hpfs.h:267 (as_abd[0x1E0] = 480-byte alsec alloc-block data area) sys/vfs/hpfs/hpfs.h:249-253 (alleaf_t 12 bytes: al_off,al_len,al_lsn) sys/vfs/hpfs/hpfs.h:233-236 (alnode_t 8 bytes: an_nextoff,an_lsn) sys/vfs/hpfs/hpfs_vfsops.c:488 (hp = kmalloc(sizeof(struct hpfsnode), M_HPFSNO, ...))
Claim
The two iteration loops in hpfs_hpbmap() use the on-disk ab_busycnt (a u_int8_t taken straight from the alblk_t embedded in a mounted HPFS fnode / alsec) without ever bounding it against the size of the container (fn_abd / as_abd). The fnode (in-kernel: struct hpfsnode, kmalloc'd) carries the entire struct fnode inline; the alsec lives in a bread'd struct buf. Forging ab_busycnt = 255 on a fnode root alblk makes the loop dereference 255 * sizeof(alleaf_t) = 3060 bytes past the 96-byte fn_abd[] data area (or 255 * sizeof(alnode_t) = 2040 bytes for the nodes variant), walking off the end of the kmalloc'd struct hpfsnode allocation and into neighbouring heap objects.
Legitimate maximum busycnt per container (data area / element size):
fnode-leaves 0x60 / sizeof(alleaf_t) = 96 / 12 = 8 fnode-nodes 0x60 / sizeof(alnode_t) = 96 / 8 = 12 alsec-leaves 0x1E0 / sizeof(alleaf_t) = 480 / 12 = 40 alsec-nodes 0x1E0 / sizeof(alnode_t) = 480 / 8 = 60
Trigger path (unprivileged user on a root-mounted crafted HPFS image):
vfs.usermount=0 β admin must mount the attacker image (realistic precond: admin has been handed a HPFS image, chowned the device) read(stat of file) β VOP_READ β hpfs_read β hpfs_hpbmap(hp, bn, &bnp, &runp) β walks fn_abd[] for i=0..ab_busycnt-1 reading alp->al_off/al_len/al_lsn β returns *bnp = (bn - alp->al_off + alp->al_lsn) computed from OOB bytes
So the OOB read is attacker-shaped two ways: (1) the fact of reading OOB corrupts an info-leak primitive β *bnp is returned to userspace via hpfs_bmap (a_doffsetp) and hpfs_read (used to drive bread), so leaked heap bytes dictate the disk offset the kernel then reads from; (2) when the OOB pointer dereference crosses an unmapped page the kernel panics (DoS).
Reproduction strategy
HPFS has no Linux/DragonFly formatter (no mkfs.hpfs/newfs_hpfs). We use TWO independent proofs:
(A) harness.c β a faithful, deterministic, userspace transcription of the
exact loops at hpfs_alsubr.c:80 and :114 against the exact on-disk
alblk_t/alleaf_t/alnode_t layouts from hpfs.h, with a poisoned allocator
(mmap a page, put the hpfsnode at the end, mark everything beyond it as
0xAA). This proves the OOB extent (2040 / 3060 bytes past the data area)
byte-for-byte, deterministically, on the default GENERIC kernel without
needing a mount.
(B) craft_img.py + live mount on the guest β constructs a minimal valid
HPFS image (SuperBlock magic, SpareBlock magic, bitmap directory + band,
root dir fnode + dirblk + one regular-file fnode whose fn_ab.ab_busycnt
is forged to 255). Root mounts it, unpriv user reads β triggers
hpfs_hpbmap OOB. See run.log / panic.txt.
Build / run
./build.sh # compiles harness.c on the guest ./run.sh # runs the harness on the guest as maxx
Expected (bug present)
harness: prints OOB_READ_BYTES_LEAF=3060 NODE=2040 and shows the loop
walking past the fnode's 96-byte fn_abd data area into the
0xAA-poisoned neighbour region (which models the next slab object).
live: kernel panic (fatal trap 12 page fault in hpfs_hpbmap) or
info leak of disk offset computed from heap bytes.
Expected (fixed)
harness reports FIXED: rejected forged busycnt (>8 leaves/>12 nodes for
fnode; >40 leaves/>60 nodes for alsec); no OOB.
live mounts + reads return EINVAL cleanly with no panic / no leak.
DF-0857 β VERDICT
Verdict: REPRODUCED (info-leak + DoS via unvalidated OOB heap read; root-only mount + unpriv read trigger; no escalation β primitive is read-only)
Status: reproduced (live kernel panic + deterministic harness)
Impact: panic (live) / leak (deterministic OOB extent proven; info-leak ceiling demonstrated by the panic mechanism β the OOB-read-derived garbage disk offset is returned to hpfs_read and used for I/O)
Confidence: certain
Mechanism (root cause, traced path:line)
hpfs_hpbmap() (sys/vfs/hpfs/hpfs_alsubr.c:62) translates a file logical
block number bn to a disk offset by walking the on-disk allocation-block
arrays. Two iteration loops drive the walk:
- sys/vfs/hpfs/hpfs_alsubr.c:80
for (i=0; i<abp->ab_busycnt; i++, anp++)(alnode variant, AB_NODES) - sys/vfs/hpfs/hpfs_alsubr.c:114
for (i=0; i<abp->ab_busycnt; i++, alp++)(alleaf variant)
abp->ab_busycnt is a u_int8_t taken straight from the on-disk alblk_t
(sys/vfs/hpfs/hpfs.h:176) embedded in either:
- a fnode's
fn_ab(the root alblk), whose data area isu_int8_t fn_abd[0x60]= 96 bytes (sys/vfs/hpfs/hpfs.h:200); or - an alsec's
as_ab, whose data area isu_int8_t as_abd[0x1E0]= 480 bytes (sys/vfs/hpfs/hpfs.h:267).
There is no validation of ab_busycnt against the size of the container
anywhere on the path. The loop bounds use the on-disk byte verbatim, so a
crafted image with ab_busycnt = 255 makes the loop dereference
255 * sizeof(alleaf_t) = 3060 bytes (leaf variant) or 255 * sizeof(alnode_t)
= 2040 bytes (node variant) past the start of fn_abd/as_abd.
Legitimate maximum ab_busycnt per container (data area / element size):
| container | element | data area | legit max | forged 255 reads |
|---|---|---|---|---|
fnode (fn_abd) |
alleaf_t | 96 B | 8 | 3060 B total, 2964 B past data |
fnode (fn_abd) |
alnode_t | 96 B | 12 | 2040 B total, 1944 B past data |
alsec (as_abd) |
alleaf_t | 480 B | 40 | 3060 B total, 2580 B past data |
alsec (as_abd) |
alnode_t | 480 B | 60 | 2040 B total, 1560 B past data |
The fnode lives inside struct hpfsnode, which is kmalloc(sizeof(struct
hpfsnode), M_HPFSNO, ...) (sys/vfs/hpfs/hpfs_vfsops.c:488). Walking 2964
bytes past fn_abd therefore crosses:
- the remaining fields of
struct fnode(fn_size, fn_reqea, fn_uid, ...); - the rest of
struct hpfsnode(h_vp, h_devvp, h_dev, h_no, h_uid, ...); - the kmalloc slab boundary into neighbouring heap objects.
The loop body at :114 reads alp->al_off, alp->al_len, alp->al_lsn for
each entry and, if bn matches, computes *bnp = bn - alp->al_off +
alp->al_lsn (sys/vfs/hpfs/hpfs_alsubr.c:122) β a value derived from the
OOB-read bytes β and returns it to the caller. hpfs_read() then passes
that garbage disk offset to bread(), which either (a) faults the buffer
cache (panic) or (b) reads an arbitrary disk sector (info leak of OOB-derived
offset into the file's read buffer).
Trigger path (unprivileged)
vfs.usermount = 0 on this guest, so mounting requires root. This is a
realistic precondition (admin mounts an attacker-supplied HPFS image, or
the image is on a removable device the admin has cause to mount). Once
mounted, the trigger is fully unprivileged:
ls /mnt # directory readdir β does NOT hit the bug cat /mnt/FILE # regular file read β VOP_READ β hpfs_read # β hpfs_hpbmap β loop walks past fn_abd β OOB read
Reproduction evidence
Live (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC, INVARIANTS ON)
craft_img.py builds a 64 KB minimal HPFS image (SuperBlock + SpareBlock +
bitmap directory + bitmap band + root dir fnode + dirblk with one "FILE"
dirent + a regular-file fnode whose fn_ab.ab_busycnt is forged to 255).
Root mounts it; unpriv user does cat /mnt/FILE. Result (run.log):
mount_hpfs -o ro /dev/vn4 /mnt ; MOUNT_RC=0 stat -f "size=%z blocks=%b" /mnt/FILE β size=65536 blocks=129 cat /mnt/FILE β HANG (kernel panic)
Panic signature (dfbsd-qemu/boot.log β panic.txt):
bgetvp: overlapr 0000000000002000/2048 0000000000002200 bp 0xfffff8004f5be060 bx 0xfffff8004f5c0010
panic: bgetvp - overlapping buffer
cpuid = 0
Trace beginning at frame 0xfffff80118a9f608
bgetvp() at bgetvp+0x129 0xffffffff806f44c9
bgetvp() at bgetvp+0x129 0xffffffff806f44c9
getblk() at getblk+0x18b 0xffffffff806d703b
breadnx() at breadnx+0x284 0xffffffff806d7714
hpfs_read() at hpfs_read+0xb3 0xffffffff82601683
vop_read() at vop_read+0x9c 0xffffffff8070a4cc
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
This is the indirect panic caused by the DF-0857 OOB read. hpfs_hpbmap
walked 255 alleaf_t entries past fn_abd[] (the bug); one of the OOB entries
satisfied the (bn >= al_off) && (bn < al_off+al_len) test against random
heap bytes, so hpfs_hpbmap returned *bnp computed from garbage. hpfs_read
passed that garbage to bread(), whose bgetvp() panicked on the overlapping
buffer offset. Reproduced twice on the unpatched #0 kernel; deterministic.
Contrast (control): the same image re-crafted with ab_busycnt=8 (the
legitimate fnode-leaves max) does not panic β cat returns EFBIG
(File too large) and the guest stays up. This proves the panic is caused
by the forged ab_busycnt=255, not by the image structure.
Deterministic harness (harness.c)
A faithful userspace transcription of the exact loops at hpfs_alsubr.c:80/:114 against the exact on-disk struct layouts from hpfs.h, with a poisoned allocator (container placed at end of an mmap'd page, next page poisoned 0xAA). Output (run.log, 3 identical runs):
[BUG] fnode leaf forged busycnt=255 total read=3060B OOB past data=2964B poison-page hit = YES [BUG] fnode node forged busycnt=255 total read=2040B OOB past data=1944B poison-page hit = YES [BUG] alsec leaf forged busycnt=255 total read=3060B OOB past data=2580B poison-page hit = YES [BUG] alsec node forged busycnt=255 total read=2040B OOB past data=1560B poison-page hit = YES [FIX] fnode leaf forged busycnt=255 result = REJECTED (EINVAL, no loop run) [FIX] fnode node forged busycnt=255 result = REJECTED (EINVAL, no loop run) [FIX] alsec leaf forged busycnt=255 result = REJECTED (EINVAL, no loop run) [FIX] alsec node forged busycnt=255 result = REJECTED (EINVAL, no loop run) DF_0857_BUG_CONFIRMED=1 DF_0857_FIX_REJECTS_FORGED_BUSYCNT=1
Exploit chain / impact ceiling
This is a read-only primitive (the loops only dereference/read the OOB elements; they don't write). No write capability β no escalation chain to uid=0. The realistic impact ceiling is:
- Kernel info leak β the OOB-read-derived disk offset (
*bnpcomputed from heap bytes) is returned to hpfs_read and used to drive bread(); if it happens to be a readable sector, the file contents returned to userspace are a function of leaked kernel heap residue (slab neighbour bytes). Repeated mounts/reads with varied forgedab_busycntandbnvalues would let an attacker sample slab-neighbour contents. - Kernel panic / DoS β when the garbage offset collides with an existing buffer (as observed live) or points at an unmapped address, the kernel panics. Deterministic on the default GENERIC kernel.
No SMAP/SMEP/KASLR-bypass chain is relevant because there is no write primitive. The valid hard blocker for escalation applies: the primitive is genuinely read-only (Phase 6 valid blocker).
Fix (fix.diff)
Validate ab_busycnt against the container maximum at every dive: point
in hpfs_hpbmap(), distinguishing fnode-root vs alsec containers via a new
in_fnode local (set to 1 at entry, cleared to 0 when diving into an
alsec). On overflow, log and return EINVAL before either loop runs.
Container maximums are derived from the on-disk data-area sizes:
#define HPFS_FN_ABD_SIZE 0x60
#define HPFS_AS_ABD_SIZE 0x1E0
#define HPFS_FN_MAX_LEAF (HPFS_FN_ABD_SIZE / sizeof(alleaf_t)) /* 8 */
#define HPFS_FN_MAX_NODE (HPFS_FN_ABD_SIZE / sizeof(alnode_t)) /* 12 */
#define HPFS_AS_MAX_LEAF (HPFS_AS_ABD_SIZE / sizeof(alleaf_t)) /* 40 */
#define HPFS_AS_MAX_NODE (HPFS_AS_ABD_SIZE / sizeof(alnode_t)) /* 60 */
Fix validation (Phase 8)
Applied fix.diff to in-guest /usr/src (patch -p1), rebuilt only the
hpfs.ko KLD module (make in /usr/src/sys/vfs/hpfs), installed to
/boot/kernel/hpfs.ko (sha256 6ab83db0d69ad8fca933ed6baaaa46f7d41db46cb63260192ab4dccffa89a9ec), rebooted, re-mounted the same
forged-busycnt=255 image, re-ran cat /mnt/FILE:
| Kernel / module | cat /mnt/FILE | dmesg | guest |
|---|---|---|---|
| #0 + unpatched hpfs.ko | HANG β kernel panic | panic: bgetvp - overlapping buffer |
DOWN |
| #0 + PATCHED hpfs.ko | EINVAL: Invalid argument |
hpfs_hpbmap: forged ab_busycnt 255 > max 8 |
UP |
Fix closes the bug: the forged ab_busycnt=255 is now rejected at the
dive-point bounds check, no OOB walk occurs, no garbage disk offset is
returned, and cat returns EINVAL cleanly with the guest remaining up.
The fix is minimal (one logical change: bounds-check ab_busycnt against
the container max before the loops) and targeted at the confirmed root
cause.
PoC changes
The runner created the entire evidence pack from scratch (the finding
folder did not exist). Files authored:
- README.md β finding summary, build/run, expected vs fixed behaviour
- harness.c β deterministic OOB-proof harness (faithful loop transcription + poisoned allocator + fixed-mode control)
- craft_img.py β HPFS image crafter (SuperBlock/SpareBlock/bitmap/dirblk/fnode with forged ab_busycnt)
- df857.img β crafted 64 KB HPFS image (forged busycnt=255)
- build.sh / run.sh β exact repro scripts
- fix.diff β git-apply-able fix (validate ab_busycnt at dive-point)
- panic.txt, run.log, run.1/2/3.log, build.log, fix_build.log, fix_run.log β full untrimmed logs
- env.txt β guest environment
- manifest.json β artifact catalog
- VERDICT.md β this file
Fix verification
fixedVALIDATED the fix: applied fix.diff to in-guest /usr/src with patch -p1 (4 hunks all succeeded), rebuilt only the hpfs.ko KLD module (make in /usr/src/sys/vfs/hpfs, rc=0, full log in fix_build.log), installed over /boot/kernel/hpfs.ko, rebooted, re-mounted the same forged-busycnt=255 image, re-ran 'cat /mnt/FILE'. Before (baseline #0 + unpatched module): kernel panic 'bgetvp - overlapping buffer' via hpfs_read, guest DOWN in DDB. After (#0 + patched module): 'cat: /mnt/FILE: Invalid argument' (EINVAL), dmesg 'hpfs_hpbmap: forged ab_busycnt 255 > max 8', guest UP. The bounds check now rejects the forged busycnt before the loop runs, eliminating the OOB walk, the garbage-disk-offset return, and the resulting panic. Fix closes the bug.
baseline (#0 unpatched): panic: bgetvp - overlapping buffer / hpfs_read() at hpfs_read+0xb3 / Stopped at Debugger+0x7c (guest DOWN). patched (#0 + fixed hpfs.ko): cat /mnt/FILE -> EINVAL 'Invalid argument' / dmesg: 'hpfs_hpbmap: forged ab_busycnt 255 > max 8' / guest UP, no panic.
Confirmed kernel references
Detail
Exploit chain
Read-only primitive (the loops only dereference/read the OOB alleaf_t/alnode_t elements; they do not write) -> no escalation chain to uid=0. The valid Phase-6 hard blocker for read-only primitives applies: there is no write capability to convert. Impact ceiling: (a) kernel info leak -- hpfs_hpbmap returns *bnp = (bn - alp->al_off + alp->al_lsn) computed from OOB heap bytes (hpfs_alsubr.c:122), and hpfs_read passes that garbage disk offset to bread(), so when it happens to be a readable sector the file's read buffer reflects leaked slab-neighbour residue; (b) deterministic kernel panic/DoS on default GENERIC -- when the garbage offset collides with an existing buffer (observed: 'panic: bgetvp - overlapping buffer' via hpfs_read->breadnx->bgetvp) the kernel panics. Live trigger: 'mount_hpfs -o ro /dev/vn4 /mnt' (root) then 'cat /mnt/FILE' (unpriv) -> panic. harness.c contains the deterministic proof; craft_img.py + df857.img the live trigger; no separate chain.c (read-only primitive, no escalation possible).
Evidence (decisive lines)
Live (dfbsd-qemu/boot.log -> panic.txt): 'panic: bgetvp - overlapping buffer' / 'hpfs_read() at hpfs_read+0xb3 0xffffffff82601683' / 'Stopped at Debugger+0x7c' -- guest DOWN in DDB. Control (busycnt=8): 'cat: /mnt/FILE: File too large' (EFBIG), guest UP. Harness (run.log, 3 identical runs): '[BUG] fnode leaf forged busycnt=255 total read=3060B OOB past data=2964B poison-page hit=YES' / 'DF_0857_BUG_CONFIRMED=1' / 'DF_0857_FIX_REJECTS_FORGED_BUSYCNT=1'. Fix (fix_run.log): 'cat: /mnt/FILE: Invalid argument' + dmesg 'hpfs_hpbmap: forged ab_busycnt 255 > max 8', guest UP.
PoC changes
Created the entire evidence pack from scratch (folder did not exist). Authored: harness.c (faithful loop transcription + poisoned allocator + fixed-mode control, 3 stress runs all identical), craft_img.py (HPFS image crafter -- SuperBlock magic FA53E9C5F995E849, SpareBlock magic FA5229C5F9911849, bitmap directory+band, dirblk D_MAGIC 77E40AAE with one 'FILE' dirent pointing at the file fnode; DragonFly-amd64 u_long=8 byte alignment for hpfsdirent.atime/ctime accounted for; file fnode forged with ab_busycnt=255), df857.img (64KB crafted image), build.sh/run.sh, fix.diff (validate ab_busycnt against container max at every dive: point -- fnode-leaves 8 / fnode-nodes 12 / alsec-leaves 40 / alsec-nodes 60 -- via an in_fnode local set on entry, cleared when diving into an alsec; returns EINVAL before the loops), VERDICT.md, manifest.json, env.txt, full untrimmed logs.
Verified recommended fix
In hpfs_hpbmap() (sys/vfs/hpfs/hpfs_alsubr.c), add container-max constants (HPFS_FN_MAX_LEAF=8, HPFS_FN_MAX_NODE=12, HPFS_AS_MAX_LEAF=40, HPFS_AS_MAX_NODE=60 derived from fn_abd[0x60]/as_abd[0x1E0] / sizeof(alleaf_t)/sizeof(alnode_t)) and a bounds check at the dive: label that rejects ab_busycnt > maxcnt for the current container (tracked via a new in_fnode local) with kprintf + EINVAL before either loop runs. Supersedes any finding-proposal (the finding folder did not contain a prior fix proposal; this fix was authored from the line-accurate root cause confirmed during verification).
Verdict
REPRODUCED. The bug is real: hpfs_hpbmap() at sys/vfs/hpfs/hpfs_alsubr.c:80 (alnode loop) and :114 (alleaf loop) bound the iteration by abp->ab_busycnt, a u_int8_t taken verbatim from the on-disk alblk_t (sys/vfs/hpfs/hpfs.h:176) embedded in fn_abd[0x60] (hpfs.h:200) or as_abd[0x1E0] (hpfs.h:267), with NO validation against the container size anywhere on the path. The fnode lives in kmalloc'd struct hpfsnode (hpfs_vfsops.c:488), so forging ab_busycnt=255 walks the loop 25512=3060B (leaf) or 2558=2040B (node) past fn_abd, crossing the kmalloc slab boundary into neighbouring heap. Confirmed two independent ways: (1) a deterministic harness transcribing the loops verbatim with a poisoned allocator shows 2964/1944/2580/1560 bytes OOB past the data area (fnode-leaf/node, alsec-leaf/node) into the 0xAA poison page; (2) a hand-crafted 64KB HPFS image (df857.img) with a regular-file fnode whose fn_ab.ab_busycnt is forged to 255, mounted root and read by an unprivileged user, panics the default GENERIC #0 kernel. The control image with busycnt=8 (legitimate max) returns EFBIG cleanly with no panic.
No comments yet.