ufs_bmaparray OOB read of indirect block buffer via unvalidated fs_nindir
Summary
ufs_bmap.c:221 daddr=((ufs_daddr_t*)bp->b_data)[xap->in_off]. in_off from ufs_getlbns:315 off=(bn/blockcnt)%MNINDIR(ump)=um_nindir=fs_nindir from disk (vfsops.c:729 NO validation). Buffer is fs_bsize bytes = fs_bsize/4 entries. Crafted fs_nindir=4096 vs fs_bsize/4=1024: in_off up to 4095 reads 12KB+ past 4KB buffer. OOB heap read. :223-228 cluster walk also OOB. OOB value used as disk block address = confused-deputy read or panic. Same root cause as DF-0894 different exploitation path (bmap read vs balloc write). Also affects ext2 (shares ufs_bmap). Fix: validate fs_nindir==fs_bsize/sizeof(ufs_daddr_t) at mount.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0914 · 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.c | trigger-source | UFS superblock fs_nindir patcher (reuses DF-0894 pattern) | 4.3 KB | view raw |
| harness.c | trigger-source | deterministic OOB-read characterization (transcribes ufs_getlbns + bap[in_off] for READ path) | 5.1 KB | view raw |
| trigger.c | trigger-source | Phase A: write at lbn=12 + ftruncate on correct image | 1.9 KB | view raw |
| trigger_ro.c | trigger-source | Phase B: read at lbn=8203 on forged image (triggers ufs_bmaparray OOB) | 1.7 KB | view raw |
| reproduce.sh | trigger-source | two-phase flow: write on correct image -> patch fs_nindir -> read on forged image | 3.5 KB | view raw |
| build.sh | build-script | builds craft_img, harness, trigger, trigger_ro | 467 B | view raw |
| run.sh | run-script | runs reproduce.sh | 136 B | view raw |
| run.log | run-log | baseline run on #0: Phase A OK, Phase B triggers panic (timeout=124) | 2.7 KB | view raw |
| fix_run.log | run-log | patched #1 kernel run: mount rejected, no panic | 3.0 KB | view raw |
| fix_build.log | build-log | full make nativekernel + installkernel output for the single-fix kernel | 5.7 MB | ↓ download |
| panic.txt | panic-signature | Fatal trap 12 page fault at ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12 | 673 B | view raw |
| dmesg.txt | dmesg | ffs_mountfs: bad fs_nindir 8192 (expected 4096) — fix validation kprintf | 96 B | view raw |
| env.txt | environment | uname, cc version, sysctls | 531 B | view raw |
| fix.diff | suggested-fix | mount-time fs_nindir validation at ffs_vfsops.c:647 (same as DF-0894) | 884 B | view raw |
| README.md | readme | summary, build/run instructions, threat model, DF-0894 relationship | 3.3 KB | ↓ raw |
| VERDICT.md | verdict | full narrative: mechanism, evidence, impact, fix validation, DF-0894 relationship | 6.4 KB | ↓ 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-0914 — ufs_bmaparray OOB read of indirect-block buffer via unvalidated fs_nindir
Summary
ufs_bmaparray() at ufs_bmap.c:221 reads daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off]
where in_off is computed by ufs_getlbns() at ufs_bmap.c:315 as
off = (bn / blockcnt) % MNINDIR(ump). MNINDIR(ump) equals ump->um_nindir,
which is copied verbatim from the on-disk fs->fs_nindir field at
ffs_vfsops.c:729 with no validation that it equals
fs_bsize / sizeof(ufs_daddr_t).
The indirect-block buffer is fs_bsize bytes = fs_bsize/4 entries. A forged
fs_nindir larger than fs_bsize/4 (e.g. 8192 vs 4096 for fs_bsize=16384)
drives in_off up to fs_nindir-1 (8191), reading up to ~16KB past the
buffer — an OOB heap READ. The OOB value is then used as a disk block address
at ufs_bmap.c:241 (daddr = blkptrtodb(ump, daddr)), a confused-deputy read.
Same root cause as DF-0894 (unvalidated fs_nindir), but a different
exploitation path: DF-0914 is the READ path (ufs_bmaparray on read/seek),
DF-0894 is the WRITE path (ffs_balloc on write). The same mount-time fix
(validating fs_nindir) closes both.
Build / Run
Build (in guest as root):
cd /root/poc && sh ./build.sh
Builds craft_img, harness, trigger (Phase A write), trigger_ro (Phase B read).
Run (in guest as root):
cd /root/poc && sh ./reproduce.sh
Expected on unpatched #0 GENERIC: Phase A succeeds (write+ftruncate on
correct fs_nindir). Phase B mounts the forged image, reads at lbn=8203 →
Fatal trap 12: page fault while in kernel mode at ufs_bmaparray+0x15c
(movslq (%rsi,%rax,4),%r12 — the bap[in_off] OOB read).
Expected on patched #1 kernel: Phase B mount rejected —
ffs_mountfs: bad fs_nindir 8192 (expected 4096) → MOUNT_B_RC=1,
no OOB, no panic.
Threat model / access
UFS mount is root-only on DragonFly (vfs.usermount=0,
SYSCAP_RESTRICTEDROOT). The attack vector is root-context mount of
attacker-supplied media → kernel OOB read. This is a root→kernel
hardening gap / info leak / DoS, not an unprivileged→root LPE.
Relationship to DF-0894
| Aspect | DF-0894 | DF-0914 |
|---|---|---|
| Root cause | unvalidated fs_nindir |
unvalidated fs_nindir |
| Sink | ffs_balloc.c:297 (bap[in_off] R/W) |
ufs_bmap.c:221 (bap[in_off] READ) |
| Path | WRITE (file extension) | READ (file read/seek) |
| Impact | OOB write/read → panic or heap corruption | OOB read → panic or confused-deputy |
| Fix | validate fs_nindir at mount |
same fix (validate fs_nindir at mount) |
DF-0894's validated fix.diff already validates
fs_nindir != fs_bsize/sizeof(ufs_daddr_t). This check closes DF-0914 too.
DF-0914's fix.diff is the same check.
Files
craft_img.c— patchesfs_nindirin UFS superblock (reuses DF-0894 pattern)harness.c— deterministic OOB-read characterization (transcribesufs_getlbns+bap[in_off])trigger.c— Phase A: write at lbn=12 + ftruncate (on correct image)trigger_ro.c— Phase B: read at lbn=8203 (on forged image)reproduce.sh— full two-phase flow (Phase A correct → Phase B forged)fix.diff— mount-timefs_nindirvalidation (closes DF-0894 + DF-0914)panic.txt— Fatal trap 12 atufs_bmaparray+0x15c(READ-path OOB proof)
DF-0914 — VERDICT
Verdict: REPRODUCED (panic), fix VALIDATED
The bug is real: fs_nindir (an on-disk UFS superblock field) is copied
verbatim into ump->um_nindir at ffs_vfsops.c:729 with no validation
that it equals fs_bsize / sizeof(ufs_daddr_t). The mount-time check at
ffs_vfsops.c:642-646 validates only fs_magic and the fs_bsize range.
A forged fs_nindir=8192 (vs the correct 4096 for fs_bsize=16384) drives
daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off] at ufs_bmap.c:221 up to
~16KB past the indirect-block buffer, because in_off = bn % MNINDIR(ump)
uses the forged value. This is the READ-path OOB (vs DF-0894's write path).
Mechanism (trigger → primitive → effect)
-
Trigger (Phase A — correct image): root
newfsa UFS1 image (fs_bsize=16384,fs_nindir=4096). Mount RW, write 1 byte at lbn=12 (offset 196608) to allocatei_ib[0](single indirect block) with correctin_off=0(in bounds).ftruncatethe file to 134283265 bytes (covers lbn=8203). Unmount cleanly. -
Trigger (Phase B — forged image): patch
fs_nindir4096→8192 in the superblock (craft_img.c). Mount RW — the forgedfs_nindiris accepted (ffs_vfsops.c:729copies it with no check). Read 1 byte at lbn=8203 (offset 134283264).ffs_read(ufs_readwrite.c:110) callsffs_blkatoff_ra(vp, offset, ...)→bread(vp, lbn=8203, ...)→VOP_BMAP→ufs_bmap→ufs_bmaparray(vp, 8203, ...). -
Primitive:
ufs_getlbns(vp, 8203)atufs_bmap.c:315computesoff = (8191 / 1) % 8192 = 8191(with forgedMNINDIR=8192). Back inufs_bmaparrayat:221:c daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off]; // bap[8191] = bp->b_data + 32764The indirect-block buffer isfs_bsize=16384bytes (4096 entries, indices 0..4095).bap[8191]reads at offset 32764 — 16380 bytes past the end of the 16384-byte buffer. -
Effect: On default GENERIC (INVARIANTS ON), the 16KB OOB read crosses into unmapped kernel virtual memory → page fault →
Fatal trap 12: page fault while in kernel modeatufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12— the exactbap[in_off]instruction. Supervisor read data, page not present — confirming it is a READ-path OOB, distinct from DF-0894's write-path OOB atffs_balloc.
Confirmed evidence
-
Panic signature (
panic.txt):Fatal trap 12: page fault while in kernel modeatufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12. Fault virtual address0xfffff80055cbdfe0, fault codesupervisor read data, page not present. Instruction pointer0xffffffff80913b7c. This is distinct from DF-0894 which panics atffs_balloc+0x5e7: movl (%rbx,%rax,4),%eax(write-path). -
Two-phase isolation: Phase A writes on the CORRECT image (fs_nindir=4096, all
ffs_ballocoperations in-bounds). Phase B only reads on the FORGED image. This cleanly separates DF-0914's read-path OOB from DF-0894's write-path OOB. -
Harness (
harness.c): transcribesufs_getlbns+ the OOB index math, confirmingin_off=8191indexesbap[8191]atbp->b_data + 32764— 16384 bytes past the 16384-byte buffer.
Impact ceiling
-
Privilege: UFS is not user-mountable on DragonFly (
vfs.usermount=0,SYSCAP_RESTRICTEDROOT). The attack vector is root-context mount of attacker-supplied media → kernel OOB read. This is a root→kernel hardening gap, not an unprivileged→root LPE. -
OOB extent: up to ~16KB (forged
fs_nindir=8192,in_off_max=8191). OOB READ atufs_bmap.c:221. The OOB value becomesdaddr, used as a disk block address at:241→ confused-deputy read of an arbitrary disk block, or page-fault panic (INVARIANTS). -
On GENERIC (INVARIANTS ON): deterministic page-fault panic for large OOB offsets. For small offsets (4 bytes past), the OOB read silently returns adjacent kernel memory (info leak) and the confused-deputy read targets an arbitrary disk block.
Why this is NOT a uid0 escalation
The trigger requires mount -t ufs, which needs root privileges
(vfs.usermount=0). A non-root user cannot mount UFS on DragonFly.
Therefore this is root→kernel corruption (a hardening gap), not
unprivileged→root. The Phase 6 escalation chain is not applicable: there is
no privilege boundary to cross (root→kernel is game-over by definition).
Fix validation (Phase 8)
fix.diff adds a check at ffs_vfsops.c:647 (after the existing
magic/bsize validation):
if (fs->fs_nindir != fs->fs_bsize / sizeof(ufs_daddr_t)) {
kprintf("ffs_mountfs: bad fs_nindir %d (expected %d)\n", ...);
error = EINVAL;
goto out;
}
This is identical to DF-0894's fix — the same mount-time fs_nindir
check closes both bugs because both depend on the forged fs_nindir being
accepted at mount.
- Before (unpatched #0): Phase B mount succeeds (
MOUNT_B_RC=0), read at lbn=8203 →Fatal trap 12atufs_bmaparray+0x15c. - After (patched #1): Phase B mount rejected —
ffs_mountfs: bad fs_nindir 8192 (expected 4096)in dmesg,mount_ufs: incorrect super block,MOUNT_B_RC=1. No OOB, no panic.
Relationship to DF-0894
Same root cause, same fix, different sink. Both bugs exploit the
unvalidated fs_nindir at ffs_vfsops.c:729:
| DF-0894 | DF-0914 | |
|---|---|---|
| Sink | ffs_balloc.c:297 (write path) |
ufs_bmap.c:221 (read path) |
| Panic RIP | ffs_balloc+0x5e7 |
ufs_bmaparray+0x15c |
| Instruction | movl (%rbx,%rax,4),%eax |
movslq (%rsi,%rax,4),%r12 |
| Trigger | file write (extension) | file read (bmap on read) |
| Fix | validate fs_nindir at mount |
same (validate fs_nindir at mount) |
DF-0894's fix.diff already contains this check. DF-0914's fix.diff is the same check (with a comment referencing both finding IDs). Applying either fix closes both bugs.
PoC changes
craft_img.c— new: patchesfs_nindir(reuses DF-0894 pattern with DF-0914-specific comments).harness.c— new: transcribesufs_getlbns+ OOB-read math for the READ path (ufs_bmaparray:221).trigger.c— new: Phase A (write 1 byte at lbn=12 + ftruncate on correct image).trigger_ro.c— new: Phase B (read at lbn=8203 on forged image).reproduce.sh— new: two-phase flow (write on correct → patch → read on forged) that cleanly isolates the READ-path OOB from DF-0894's write path.fix.diff— mount-timefs_nindirvalidation (same as DF-0894).
Fix verification
fixedVALIDATED the fix: on the unpatched #0 baseline (6.5-DEVELOPMENT #0), DF-0914 reproduces -- Phase A writes+ftruncate on correct fs_nindir=4096 succeeds, Phase B patches to 8192 and mounts, read at lbn=8203 triggers Fatal trap 12 page fault at ufs_bmaparray+0x15c (supervisor READ data, page not present). On the single-fix #1 kernel (same source + the fs_nindir validation check at ffs_vfsops.c:647), Phase B mount is REJECTED -- dmesg shows 'ffs_mountfs: bad fs_nindir 8192 (expected 4096)', MOUNT_B_RC=1, no OOB read, no panic. The fix closes DF-0914 (and also DF-0894, same root cause).
BEFORE (baseline #0): MOUNT_B_RC=0 (mount succeeds with forged fs_nindir=8192) -> read at lbn=8203 -> Fatal trap 12 page fault at ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12 (OOB READ 16KB past buffer). AFTER (patched #1): mount_ufs: incorrect super block, MOUNT_B_RC=1 (mount rejected). dmesg: ffs_mountfs: bad fs_nindir 8192 (expected 4096). No panic, no OOB -- trigger_ro fails to open file (mount never happened).
Confirmed kernel references
Detail
Exploit chain
N/A (valid hard blocker -- root-only reachability). The trigger requires mount -t ufs which needs root privileges (vfs.usermount=0, SYSCAP_RESTRICTEDROOT). This is a root->kernel OOB read, not an unprivileged->root escalation. The OOB read at ufs_bmap.c:221 reads up to 16KB past the indirect-block buffer; the OOB value becomes a disk block address used at :241 (blkptrtodb) -- a confused-deputy read of an arbitrary disk block or page-fault panic. Impact ceiling: DoS (deterministic panic on GENERIC with large OOB offsets) or info leak (for small offsets that read adjacent kernel memory without faulting). No chain file written -- this is a non-corruption-class finding (root->kernel OOB read / hardening gap).
Evidence (decisive lines)
=== BASELINE (#0 unpatched) panic === Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff80055cbdfe0 / fault code = supervisor read data, page not present / instruction pointer = 0x8:0xffffffff80913b7c / Stopped at ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12 / db>. === PATCHED (#1) fix validation === mount_ufs: /dev/vn0 on /mnt/test: incorrect super block / MOUNT_B_RC=1 / (dmesg) ffs_mountfs: bad fs_nindir 8192 (expected 4096) / No panic, no OOB -- mount rejected at the validation check.
PoC changes
Created findings/poc/DF-0914/ from scratch. craft_img.c reuses DF-0894's fs_nindir patcher pattern. harness.c transcribes ufs_getlbns + the READ-path bap[in_off] OOB math. trigger.c (Phase A) writes 1 byte at lbn=12 + ftruncates on the CORRECT image (avoids triggering DF-0894's write-path OOB). trigger_ro.c (Phase B) reads at lbn=8203 on the FORGED image (triggers ufs_bmaparray:221 OOB). reproduce.sh implements the two-phase flow: newfs->write->ftruncate->unmount on correct fs_nindir, then patch->mount->read on forged fs_nindir. This cleanly isolates DF-0914's read-path OOB from DF-0894's write-path OOB. fix.diff validates fs_nindir at mount (identical to DF-0894's fix -- same root cause, same check closes both).
Verified recommended fix
Add a mount-time validation at ffs_vfsops.c:647 (after the existing fs_magic/fs_bsize check): if (fs->fs_nindir != fs->fs_bsize / sizeof(ufs_daddr_t)) { error = EINVAL; goto out; }. This rejects forged fs_nindir values at mount, preventing the OOB index in both ufs_bmaparray:221 (DF-0914 read path) and ffs_balloc:297 (DF-0894 write path). Matches DF-0894's fix proposal -- the same check closes both bugs because both depend on the forged fs_nindir being accepted at mount. The full git-apply-able diff lives in findings/poc/DF-0914/fix.diff.
Verdict
REPRODUCED. The bug is real: fs_nindir (an on-disk UFS superblock field) is copied verbatim into ump->um_nindir at ffs_vfsops.c:729 with NO validation that it equals fs_bsize/sizeof(ufs_daddr_t). The mount-time check at ffs_vfsops.c:642-646 validates only fs_magic and fs_bsize range. A forged fs_nindir=8192 (vs correct 4096 for fs_bsize=16384) drives daddr=((ufs_daddr_t*)bp->b_data)[xap->in_off] at ufs_bmap.c:221 up to ~16KB past the indirect-block buffer because in_off = bn % MNINDIR(ump) uses the forged value. Confirmed by Fatal trap 12 page fault at ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12 (supervisor READ data, page not present) -- the exact bap[in_off] READ instruction, DISTINCT from DF-0894's write-path panic at ffs_balloc+0x5e7. Two-phase PoC (write on correct image -> patch fs_nindir -> read on forged image) cleanly isolates the READ-path OOB from DF-0894's write path.
No comments yet.