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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0857 Β· 19 files
FileTypeDescriptionSize
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
README.md readme finding summary, build/run, expected vs fixed behaviour
↓ download 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.

VERDICT.md verdict full narrative: mechanism, trigger, exploit-chain ceiling, fix validation
↓ download raw

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:

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:

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:

  1. the remaining fields of struct fnode (fn_size, fn_reqea, fn_uid, ...);
  2. the rest of struct hpfsnode (h_vp, h_devvp, h_dev, h_no, h_uid, ...);
  3. 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:

  1. Kernel info leak β€” the OOB-read-derived disk offset (*bnp computed 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 forged ab_busycnt and bn values would let an attacker sample slab-neighbour contents.
  2. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; hpfs.ko module rebuilt from patched source at /usr/src/sys/vfs/hpfs and installed to /boot/kernel/hpfs.ko, sha256 6ab83db0d69ad8fca933ed6baaaa46f7d41db46cb63260192ab4dccffa89a9ec)

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.