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.