Heap OOB read in sparing-table scan rt_l unbounded by st_size
Summary
udf_vfsops.c:662 kmalloc(st_size). :681 bcopy st_size bytes. :692 for(i=0;i<s_table->rt_l;i++) bounded by rt_l uint16 NOT st_size. entries[] starts at offset 56 each 8B. st_size=64 rt_l=1000 reads ~8KB past 64B heap. s_table_entries=i set from OOB entries drives udf_translate further OOB. Mount+file I/O across sparing region triggers.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0881 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_evil_udf.py | trigger-source | Python UDF image crafter: st_size=32768, rt_l=65535 | 5.8 KB | view raw |
| evil.udf | trigger-binary | Crafted UDF image (647168 bytes) | 632.0 KB | β download |
| build.sh | build-script | Runs craft_evil_udf.py to produce evil.udf | 366 B | view raw |
| run.sh | run-script | vnconfig + mount -t udf commands | 794 B | view raw |
| fix.diff | suggested-fix | Bound rt_l by max_entries=(st_size-56)/8; validate st_size | 1.5 KB | view raw |
| VERDICT.md | verdict | Full analysis: mechanism, evidence, fix validation | 6.3 KB | β raw |
| README.md | readme | Summary and reproduce instructions | 1.6 KB | β raw |
| run.log | run-log | Unpatched #0 kernel panic output | 1.9 KB | view raw |
| fix_run.log | run-log | Patched #1 kernel clean output (2 runs) | 960 B | view raw |
| fix_build.log | build-log | Single-fix kernel build log (nativekernel rc=0) | 5.6 MB | β download |
| panic.txt | panic-signature | Fatal trap 12: page fault in udf_mount.part.2+0x8c9 | 401 B | view raw |
| env.txt | environment | Guest uname, kernel hashes, slab allocator config | 1.1 KB | 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-0881 β Heap OOB read in sparing-table scan: rt_l unbounded by st_size
Summary
In udf_find_partmaps() (sys/vfs/udf/udf_vfsops.c:692), the sparing-table
entry scan loop iterates rt_l times (a uint16_t from the on-disk table),
but rt_l is never bounded by st_size (the declared table size used for
kmalloc). A crafted UDF image with rt_l >> st_size causes the loop to read
entries[i] far past the allocation β a heap OOB read / info-leak / panic.
Reproduce
# On the DragonFlyBSD guest (root):
./build.sh # crafts evil.udf (st_size=32768, rt_l=65535)
kldload udf # load UDF module
mkdir -p /mnt/udf
./run.sh # vnconfig + mount β panic on unpatched kernel
Expected behavior
- Unpatched kernel (
#0): mount hangs / guest panics withFatal trap 12: page fault ... Stopped at udf_mount.part.2+0x8c9 - Fixed kernel (
#1): mount returnsEINVALcleanly, guest stays up
Files
| File | Description |
|---|---|
craft_evil_udf.py |
Python UDF image crafter (parameterized st_size/rt_l) |
evil.udf |
Crafted image: st_size=32768, rt_l=65535 |
build.sh |
Runs the crafter |
run.sh |
vnconfig + mount commands |
fix.diff |
git-apply-able fix: bound rt_l by st_size |
VERDICT.md |
Full analysis: mechanism, evidence, fix validation |
run.log |
Unpatched kernel panic output |
fix_run.log |
Patched kernel clean output |
fix_build.log |
Single-fix kernel build log |
panic.txt |
Panic signature from serial console |
env.txt |
Guest environment |
manifest.json |
Machine-readable catalog |
DF-0881 β Heap OOB read in sparing-table scan: rt_l unbounded by st_size
Verdict: REPRODUCED (panic on unpatched kernel) + FIX VALIDATED
Severity: Medium (heap OOB read / info-leak / mount-time DoS via crafted UDF image) Confidence: certain Status: reproduced β fix authored β fix built β fix validated (before/after on single-fix kernel)
Mechanism
The bug is in udf_find_partmaps() (sys/vfs/udf/udf_vfsops.c), called during
UDF mount when the Logical Volume Descriptor contains a Type 2 Sparable Partition
Map. The flow:
-
udf_vfsops.c:662βudfmp->s_table = kmalloc(pms->st_size, M_UDFMOUNT, M_WAITOK | M_ZERO);Allocatesst_sizebytes for the sparing table.st_sizecomes from the on-disk partition map (struct part_map_spare.st_size, auint32_t). -
udf_vfsops.c:681βbcopy(bp->b_data, udfmp->s_table, pms->st_size);Copiesst_sizebytes from the disk buffer into the allocation. This copy is correctly bounded byst_size. -
udf_vfsops.c:692β THE BUG:c for (i = 0; i < udfmp->s_table->rt_l; i++) { udfmp->s_table_entries = i; if (udfmp->s_table->entries[i].org >= 0xfffffff0) break; }The loop iteratesrt_ltimes (fromstruct udf_sparing_table.rt_l, auint16_tread from the on-disk sparing table).rt_lis NOT validated againstst_size. Sinceentries[]starts at offset 56 (offsetof(struct udf_sparing_table, entries)= tag(16)+regid(32)+rt_l(2)+ reserved(2)+seq_num(4) = 56) and each entry is 8 bytes (sizeof(struct spare_map_entry)= org(4)+map(4)), the loop reads: -entries[0]at offset 56 (valid ifst_size >= 64) -entries[k]at offset56 + 8*kβ OOB when56 + 8*k >= st_size
With st_size=64 and rt_l=65535, entries[1] (offset 64) is already OOB,
and the loop reads up to offset 524,328 β ~512 KB past a 64-byte allocation.
- Downstream impact β
udfmp->s_table_entries = iis set from the OOB-derived loop counter, then used inudf_translate()(udf_vnops.c:1167) to scan the sameentries[]array again for sector remapping, compounding the OOB.
Reproduction
PoC: crafted UDF image (evil.udf)
A Python script (craft_evil_udf.py) generates a minimal UDF image with:
- Anchor VDP at sector 256 β VDS at sector 0
- Partition Descriptor (TAGID_PARTITION=5) at sector 0
- Logical Volume Descriptor (TAGID_LOGVOL=6) at sector 1 with a Type 2 Sparable
Partition Map: st_size=32768, st_loc[0]=34, packet_len=2048
- Sparing Table at sector 34: tag id=0, rt_l=65535, 1 entry (org=0)
st_size=32768 forces the allocation through the kmem page-zone (allocations >
ZALLOC_ZONE_LIMIT=16384 get dedicated pages), so the OOB read quickly hits an
unmapped page β deterministic panic.
Before (unpatched kernel #0):
vnconfig -c vn0 evil.udf mount -t udf -o rdonly /dev/vn0 /mnt/udf
β Kernel panic:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff80118632000 fault code = supervisor read data, page not present current process = 952 (mount) Stopped at udf_mount.part.2+0x8c9: cmpl $-0x11,0x38(%rsi,%rdx,8)
The disassembly cmpl $-0x11,0x38(%rsi,%rdx,8) is exactly entries[i].org >=
0xfffffff0: offset 0x38=56, %rsi=s_table, %rdx=i, $-0x11=0xFFFFFFEF
(the >= compiles to > 0xFFFFFFEF). udf_mount.part.2 is GCC's name for
udf_find_partmaps() partially inlined into udf_mount().
After (patched kernel #1):
Same PoC β mount_udf: /dev/vn0: Invalid argument (EINVAL), guest alive, no
panic. The EINVAL is from the intentionally-missing File Set Descriptor, not from
the bug. The sparing-table scan completed within bounds (no OOB read).
Exploit chain
Class: heap OOB read (read-only primitive). No write capability.
Escalation: none possible β this is a pure read primitive. The OOB read leaks
kernel heap data (~512 KB of adjacent slab/kmem objects per mount attempt) and
corrupts s_table_entries (driving further OOB reads in udf_translate). On the
GENERIC kernel (INVARIANTS ON), this manifests as a panic when the read crosses a
page boundary; with smaller st_size (slab-zone allocation), the OOB reads
mapped heap silently (info leak).
Impact ceiling: Mount-time heap info-leak + DoS (panic). Realistic
precondition: root mounts an attacker-supplied UDF image (or
vfs.usermount=1 + root-created image owned by the attacker).
Fix
fix.diff β two changes in udf_find_partmaps():
-
Validate
st_sizebeforekmalloc: reject ifst_size < sizeof(struct udf_sparing_table)(header + at least 1 entry must fit). -
Bound the entry scan loop by the number of entries that actually fit in
st_size:c max_entries = (pms->st_size - offsetof(struct udf_sparing_table, entries)) / sizeof(struct spare_map_entry); for (i = 0; i < udfmp->s_table->rt_l && i < max_entries; i++) {
Fix validation (Phase 8)
| Phase | Kernel | Result |
|---|---|---|
Baseline (unpatched #0) |
6.5-DEVELOPMENT #0 |
PANIC β page fault in udf_mount.part.2+0x8c9 |
Patched (#1, fix applied) |
6.5-DEVELOPMENT #1 |
CLEAN β EINVAL, no panic, guest alive (Γ2 runs) |
The fix is deterministic: rt_l is now clamped to max_entries, the scan stays
within the allocation, and no page fault occurs.
PoC changes
Authored from scratch (no prior PoC existed):
- craft_evil_udf.py β Python UDF image crafter (parameterized st_size/rt_l)
- evil.udf β crafted image with st_size=32768, rt_l=65535
- build.sh / run.sh β exact reproduction commands
- fix.diff β git-apply-able fix (validated on built+booted kernel)
Kernel references
sys/vfs/udf/udf_vfsops.c:662βkmalloc(pms->st_size)allocationsys/vfs/udf/udf_vfsops.c:681βbcopy(..., pms->st_size)(correctly bounded)sys/vfs/udf/udf_vfsops.c:692β THE BUG:for (i=0; i<s_table->rt_l; i++)unbounded by st_sizesys/vfs/udf/udf_vfsops.c:694βentries[i].orgOOB readsys/vfs/udf/ecma167-udf.h:263-270βstruct udf_sparing_tablelayout (entries at offset 56)sys/vfs/udf/ecma167-udf.h:234-246βstruct part_map_spare(st_size field)sys/vfs/udf/udf_vnops.c:1167β downstreamudf_translatescan using corrupteds_table_entries
Fix verification
fixedVALIDATED the fix: the same evil.udf (st_size=32768, rt_l=65535) causes a page-fault PANIC on the unpatched #0 baseline (Stopped at udf_mount.part.2+0x8c9) and does NOT panic on the single-fix #1 kernel (mount returns EINVAL cleanly, guest stays alive, sparing scan bounded by max_entries). Tested twice on patched kernel -- deterministic. The fix clamps the entry-scan loop to i < max_entries where max_entries=(st_size-56)/8, preventing any OOB read regardless of the on-disk rt_l value.
BASELINE (#0 unpatched): Fatal trap 12: page fault while in kernel mode / fault virtual address=0xfffff80118632000 / Stopped at udf_mount.part.2+0x8c9: cmpl $-0x11,0x38(%rsi,%rdx,8) / db> (guest dead). PATCHED (#1 fix applied): mount_udf: /dev/vn0: Invalid argument / MOUNT_RC=1 / Guest alive: up 2 mins / dmesg: 'Couldn't find the fsd' (scan completed within bounds, no OOB).
Confirmed kernel references
Detail
Exploit chain
Class: heap OOB read (read-only primitive). No write capability, so no uid=0 escalation is possible. The OOB read leaks ~512KB of adjacent kernel heap data per mount attempt and corrupts s_table_entries (set from OOB-derived loop counter), which drives further OOB reads in udf_translate() (udf_vnops.c:1167) during subsequent file I/O. On the GENERIC kernel (INVARIANTS ON), with a kmem-page-zone allocation (st_size=32768), the OOB read deterministically hits an unmapped page and panics. With smaller st_size (slab-zone allocation, e.g. 64), the OOB reads mapped slab pages silently (info-leak without panic). Impact ceiling: mount-time heap info-leak + DoS (panic). Realistic precondition: root mounts attacker-supplied UDF image (or vfs.usermount=1 + root-created image owned by attacker).
Evidence (decisive lines)
UNPATCHED #0: Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff80118632000 / fault code = supervisor read data, page not present / current process = 952 (mount) / Stopped at udf_mount.part.2+0x8c9: cmpl $-0x11,0x38(%rsi,%rdx,8) / db>. PATCHED #1: mount_udf: /dev/vn0: Invalid argument (MOUNT_RC=1), guest alive (uptime 2 mins), dmesg: 'Couldn't find the fsd' (scan bounded, no OOB).
PoC changes
Authored the entire evidence pack from scratch (no prior PoC existed): craft_evil_udf.py (Python UDF image crafter, parameterized st_size/rt_l), evil.udf (crafted image with st_size=32768, rt_l=65535), build.sh/run.sh repro scripts, fix.diff (bound rt_l by max_entries=(st_size-56)/8), VERDICT.md, manifest.json. The image places a minimal Anchor+VDS(PD+LVD with Type-2 Sparable Partition Map)+Sparing Table to reach udf_find_partmaps during mount.
Verified recommended fix
In udf_find_partmaps() at sys/vfs/udf/udf_vfsops.c: (1) validate st_size >= sizeof(struct udf_sparing_table) before kmalloc (reject tiny/forged sizes); (2) compute max_entries = (st_size - offsetof(udf_sparing_table, entries)) / sizeof(spare_map_entry) and change the loop at :692 from 'for (i=0; i
Verdict
REPRODUCED. The bug at sys/vfs/udf/udf_vfsops.c:692 is real and confirmed: the sparing-table entry scan loop iterates rt_l times (uint16 from on-disk table) WITHOUT bounding rt_l against st_size (the kmalloc allocation size). A crafted UDF image with st_size=32768 and rt_l=65535 causes the loop to read entries[4089..65534] -- 491KB past the 32KB kmem allocation -- triggering a page fault. Confirmed by: 'Fatal trap 12: page fault while in kernel mode, fault virtual address=0xfffff80118632000, page not present, Stopped at udf_mount.part.2+0x8c9: cmpl $-0x11,0x38(%rsi,%rdx,8)' -- that instruction is exactly entries[i].org >= 0xfffffff0 at offset 0x38=56 (offsetof entries), with %rsi=s_table and %rdx=i (the unbounded counter). udf_mount.part.2 is GCC's name for udf_find_partmaps() inlined into udf_mount().
No comments yet.