Embedded-ICB read path trusts unvalidated l_ea/l_ad: udf_vget's wrapping int allocation plus udf_readatoffset's &fentry->data[l_ea] with sign-extended negative *size yields a wild OOB kernel-heap read
Summary
udf_vget (udf_vfsops.c:527) computes size = UDF_FENTRY_SIZE + fe->l_ea + fe->l_ad in wrapping 32-bit arithmetic from disk fields, so crafted values (l_ea=0x1000, l_ad=0xFFFFF150 -> int 0x200) allocate a 512-byte M_UDFFENTRY object while the copied fields stay huge; udf_readatoffset's embedded path (:1037-1039) then returns *data = &fentry->data[0x1000] (~3.7KB past the allocation) and *size = (int)0xFFFFF150 = -5936, which udf_read passes to uiomove as (size_t)-5936 -> copies user-chosen byte counts from out-of-bounds heap. With l_ea=0x40000000 the kernel demonstrably dereferences a pointer 1GB past the object (EFAULT). Distinct sink from pass-1 DF-0880 (vget bcopy source over-read). Crafted UDF image mounted by root -> unpriv open()+read() of the embedded file -> deterministic kernel heap disclosure at attacker-chosen offset (full 32-bit range past a small object) and length. VERIFIED guest uid 1001: EMB read returns 96 bytes of kernel heap from fentry+4272 (3x + 1024/4096-byte reads); EMB2 (l_ea=0x40000000) -> EFAULT proving the wild dereference. Fix validated (vget l_ea/l_ad validation + embedded-path offset handling + size==0 break): patched module rejects EMB/EMB2 with EINVAL before any allocation/read.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3103 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.py | β | 10.9 KB | view raw | |
| df3102.udf | β | 1.0 MB | β download | |
| poc.c | β | 6.2 KB | view raw | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 89 B | view raw | |
| run.log | β | 27.5 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| df31xx_combined_fix.diff | β | 1.9 KB | view raw | |
| fix_validate.sh | β | 2.5 KB | view raw | |
| fix_build.log | β | 7.4 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw | |
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 3.9 KB | β raw | |
| env.txt | β | 236 B | view raw | |
| leak_sample.txt | β | 2.3 KB | view raw |
DF-3103 β embedded-ICB read uses unvalidated l_ea/l_ad: wild OOB heap read
What
Two cooperating defects:
udf_vget()(sys/vfs/udf/udf_vfsops.c:527) computes the file-entry copy size assize = UDF_FENTRY_SIZE + fe->l_ea + fe->l_adin wrapping 32-bit int arithmetic from fully attacker-controlled disk fields, thenkmalloc(size)+bcopy. Craftedl_ea/l_adwrap the sum to a small value (e.g.176 + 0x1000 + 0xFFFFF150 = 0x100000200β int0x200) so theM_UDFFENTRYallocation is only 512 bytes while the on-disk fields stay huge in the copied structure.- The embedded-ICB path of
udf_readatoffset()(sys/vfs/udf/udf_vnops.c:1037-1039) then does*data = &fentry->data[l_ea]; *size = l_ad;with no bounds check and no offset handling. With the wrapped fields:*data= 4272 bytes into a 512-byte allocation,*size=(int)0xFFFFF150 = -5936, whichudf_read()'suiomove(data, size, uio)(:403) receives as(size_t)-5936β it happily movesuio_residbytes of kernel heap from far past the allocation into the user's buffer.
Also demonstrated with l_ea = 0x40000000 (file EMB2): *data lands ~1 GB
past the object; uiomove()'s fault handling turns the unmapped wild read
into EFAULT β proving the kernel dereferences the wild pointer.
Related pass-1 finding: DF-0880 covers the bcopy source over-read at the
same udf_vget site; this finding is the distinct vnops sink: the
integer-wrap β undersized allocation β &fentry->data[l_ea] + negative
*size β sign-extended uiomove chain in udf_readatoffset()/udf_read().
Reproduce
./run.sh # mounts the crafted image (root), then as uid 1001:
# - 3x read(EMB, 96) -> 96 OOB heap bytes each
# - read(EMB, 1024/4096)
# - read(EMB2, 96) -> EFAULT (wild pointer +1GB)
Expected (stock kernel)
read() returned 96 bytes from embedded file (requested 96) DF3103_LEAK_CONFIRMED: 96 bytes copied from kernel heap at fentry->data[0x1000] (~3.7KB past a 512-byte M_UDFFENTRY allocation) ... read(96) failed: Bad address <- EMB2 (l_ea=0x40000000)
Patched kernel (fix.diff: vget validation + embedded-path offset handling):
open /mnt/EMB: Invalid argument β the malformed file entry is rejected
before any allocation or read.
Fix validation
See DF-3102/fix_validate.sh + df31xx_combined_fix.diff (shared driver for DF-3102/3103/3104); fix_run.log excerpt in this directory.
DF-3103 β VERDICT
Status: REPRODUCED (leak / wild-pointer read) β crafted UDF image mounted
by root gives any unprivileged user an out-of-bounds kernel-heap read (and a
provably wild pointer dereference) via plain open()+read().
Root cause chain
sys/vfs/udf/udf_vfsops.c:527-530:
size = UDF_FENTRY_SIZE + fe->l_ea + fe->l_ad; /* wraps in u32/int */
unode->fentry = kmalloc(size, M_UDFFENTRY, M_WAITOK | M_ZERO);
bcopy(bp->b_data, unode->fentry, size);
Crafted FE (icbtag.flags & 7 == 3, embedded data): l_ea = 0x1000,
l_ad = 0xFFFFF150 β sum 0x100000200 β size = 0x200 β a 512-byte
allocation that still carries the huge raw fields.
sys/vfs/udf/udf_vnops.c:1037-1039 (embedded path of udf_readatoffset):
fentry = node->fentry;
*data = &fentry->data[fentry->l_ea]; /* fentry + 176 + 4096 */
*size = fentry->l_ad; /* (int)0xFFFFF150 = -5936 */
No validation of l_ea/l_ad against the allocation, no offset handling,
no negative-size guard. udf_read() (:401-403) passes both to
uiomove(data, size, uio); the negative int length sign-extends to a huge
size_t, so uiomove moves uio_resid (user-chosen) bytes from
fentry + 4272 β ~3.7 KB past the 512-byte M_UDFFENTRY object.
Reproduction (run.log)
Crafted image: EMB FE at partition-sector 3 with the fields above,
inf_len = 0x10000; EMB2 FE with l_ea = 0x40000000,
l_ad = 0xC0000140 (same 512-byte wrap). Runs as su maxx (uid 1001):
read(EMB, 96)Γ3: 96 bytes returned each time from kernel heap past the allocation (DF3103_LEAK_CONFIRMEDΓ3, deterministic).read(EMB, 1024)andread(EMB, 4096): same, no fault β the 512-byte malloc zone is backed by large slabs, so the overread stays mapped.read(EMB2, 96):EFAULT(Bad address) βuiomovefaulted readingfentry->data[0x40000000](1 GB past the object); DF's copy-fault handling converted the kernel-mode wild read into EFAULT instead of a panic. This is direct proof the kernel dereferenced the attacker-chosen wild offset.
A "correct" kernel would have rejected this file entry outright (an FE is
one block; l_ea + l_ad cannot legitimately reach 2^32) β see fix.
Impact
- Crafted-media heap OOB read with attacker-chosen offset (
l_eafully controlled: any offset up to 4 GB past a small heap object) and attacker-chosen length (read count). Deterministic leak of kernel heap; an unmapped choice yields EFAULT (or, on kernels without the copy-fault guard, a kernel page fault/panic β DoS). - Read primitive only β no write, so no direct uid=0 chain from this bug alone. Combined with DF-0831-style write primitives on the same read-only-image attack surface, it provides the info-leak half of an exploit pair (heap layout disclosure).
Fix validation (fix.diff)
udf_vget(): rejectl_ea/l_adthat don't fit in one logical block (bsize - UDF_FENTRY_SIZE), which kills the wrap and the DF-0880 bcopy over-read at once.udf_readatoffset()embedded path: FreeBSD-style offset handling (*data += offset; *size -= offset;/*size = 0past EOF).udf_read():breakonsize == 0so a short embedded file cannot spin the loop.
With the patched module (fix_run.log, same image, same PoC):
--- DF-3103 on patched kernel (expect open EMB to fail) --- open /mnt/EMB: Invalid argument --- DF-3103 EMB2 on patched kernel (expect open to fail) --- open /mnt/EMB2: Invalid argument
No allocation, no read, clean EINVAL β behavior eliminated at the root.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedCombined fix (vget validation + embedded-path offset handling + zero-size loop break) applied, udf.ko rebuilt, same image remounted: open('/mnt/EMB') and open('/mnt/EMB2') now fail with EINVAL at vget before any allocation or read; no OOB access occurs (fix_run.log).
['fix_run.log', 'fix_build.log', 'fix.diff', 'df31xx_combined_fix.diff']
Confirmed kernel references
Detail
Exploit chain
crafted UDF image (attacker-supplied media or downloaded image mounted by root) -> unprivileged open()+read() of the embedded file -> deterministic kernel heap disclosure at attacker-chosen offset/length; wild-offset variant yields EFAULT/page-fault (DoS on kernels without copy-fault guards). Read primitive only; serves as the info-leak half of an exploit pair with the DF-0831 write primitive on the same surface.
Evidence (decisive lines)
["run.log: 'read() returned 96 bytes from embedded file' x3 + 'DF3103_LEAK_CONFIRMED' x3 (deterministic), 1024- and 4096-byte reads also returned OOB heap", "run.log: 'read(96) failed: Bad address' for EMB2 (l_ea=0x40000000 -> EFAULT on the wild kernel read)", "fix_run.log: 'open /mnt/EMB: Invalid argument' and 'open /mnt/EMB2: Invalid argument' on the patched module", 'craft_img.py: EMB l_ea=0x1000 l_ad=0xFFFFF150 (176+...=0x100000200 -> int 0x200); EMB2 l_ea=0x40000000 l_ad=0xC0000140']
PoC changes
Seed PoC authored fresh for pass 2 (extends the DF-0831 crafter). One iteration: added
Verified recommended fix
Validate l_ea/l_ad in udf_vget against bsize - UDF_FENTRY_SIZE (FE is one block), and give udf_readatoffset()'s embedded path offset/remaining-length handling (FreeBSD-style).
Verdict
udf_vget() computes the fentry allocation size as UDF_FENTRY_SIZE + l_ea + l_ad in wrapping 32-bit arithmetic from disk-controlled fields, so crafted l_ea/l_ad wrap it to a small kmalloc while udf_readatoffset()'s embedded-ICB path (udf_vnops.c:1037-1039) still uses &fentry->data[l_ea] with *size = l_ad (sign-extended negative int); uiomove() then copies user-chosen byte counts from up to 4 GB past the undersized M_UDFFENTRY allocation. Reproduced as uid 1001: read(EMB, 96/1024/4096) each returned OOB kernel heap bytes; EMB2 with l_ea=0x40000000 produced EFAULT, proving the kernel dereferences the attacker-chosen wild pointer. Patched module rejects the malformed file entry with EINVAL at vget.
No comments yet.