Stale dereference of rootp after pribp released β use-after-free style read on buffer cache recycle
Summary
cd9660_vfsops.c:435-437 pribp->b_flags|=B_AGE brelse(pribp) pribp=NULL. rootp=pri->root_directory_record :407-410 was derived from pribp->b_data. :466 isonum_711(rootp->ext_attr_length) reads from released buffer B_AGE preferentially evicted by concurrent I/O. ext_attr_length u8 added to root_extent wrong block for RRIP detection. Fix: cache ext_attr_length in local before brelse.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0851 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | ISO mount loop with concurrent I/O pressure to try to trigger the stale-read race | 4.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o poc poc.c | 149 B | view raw |
| run.sh | run-script | creates ISO, vnconfig, runs PoC as root (cd9660 requires RESTRICTEDROOT) | 1.4 KB | view raw |
| README.md | readme | bug mechanism, build/run/expected, reachability note | 2.9 KB | β raw |
| VERDICT.md | verdict | full analysis: code-level trace, why race doesn't trigger, impact ceiling, fix | 5.7 KB | β raw |
| fix.diff | suggested-fix | cache ext_attr_length in local before brelse; eliminates stale read by construction | 1.2 KB | view raw |
| run.log | run-log | unpatched #0 kernel: 3000 mounts, 0 failures | 601 B | view raw |
| fix_run.log | run-log | patched #1 kernel: 3000 mounts, 0 failures (identical behavior) | 601 B | view raw |
| fix_build.log | build-log | single-fix kernel build + installkernel, full output, rc=0 | 5.6 MB | β download |
| env.txt | environment | uname, cc version, vfs.usermount, vnconfig state | 266 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 |
DF-0851 β Stale dereference of rootp after pribp released
Finding
File: sys/vfs/isofs/cd9660/cd9660_vfsops.c lines 435β466
Severity: Low
Title: Stale dereference of rootp after pribp released β use-after-free style read on buffer cache recycle
Bug mechanism
In iso_mountfs():
1. Line 329β347: bread(devvp, ...) reads the ISO volume descriptor into buffer pribp. pri = (struct iso_primary_descriptor *)vdp where vdp = (struct iso_volume_descriptor *)pribp->b_data. So pri points into the buffer cache data of pribp.
2. Line 407β410: rootp = (struct iso_directory_record *)pri->root_directory_record β rootp points into pribp->b_data.
3. Line 428β430: bcopy(rootp, isomp->root, ...) and isomp->root_extent = isonum_733(rootp->extent) β valid reads before release.
4. Line 435β437: pribp->b_flags |= B_AGE; brelse(pribp); pribp = NULL; β the buffer is released back to the buffer cache free list, marked B_AGE for preferential eviction.
5. Line 466: isonum_711(rootp->ext_attr_length) β STALE READ. rootp still points into the released buffer's data area. If the buffer has been evicted and reused by a concurrent bread() for a different block, this reads stale/wrong data.
The B_AGE flag explicitly requests preferential eviction, making the race more likely than a plain brelse.
Impact ceiling
- Worst case: If the race triggers,
rootp->ext_attr_lengthreturns a wrong byte βisomp->root_extent + wrong_byteis computed βbread()reads the wrong block from the device β either an EIO (mount fails β DoS) or wrong RRIP detection (mount succeeds but with wrong Rock Ridge behavior β correctness bug). - NOT memory corruption: The read is from valid buffer-cache memory (not freed to slab), just potentially stale data. No UAF primitive, no escalation chain possible.
- Race window: Between
brelse(pribp)(line 436) andisonum_711(rootp->ext_attr_length)(line 466), the code only does field assignments β no I/O. Only concurrentbread()/geteblk()from another thread/CPU could evict the buffer in that window. The window is extremely narrow.
Build
./build.sh
Run
./run.sh
This script creates a valid ISO image, enables vfs.usermount, chowns the image to the unprivileged user, then runs the PoC as maxx with concurrent I/O pressure to try to trigger the race.
Expected behavior
- Bug present (unpatched): The race is extremely unlikely to trigger β the window has no I/O. The PoC reports "no mount failures observed" in the vast majority of runs. This is a code-level defect, not a reliably-triggerable runtime fault.
- Bug fixed (patched): The stale read is eliminated by caching
ext_attr_lengthbeforebrelse. Behavior is identical at runtime (the race didn't trigger anyway), but the code is now correct by construction.
DF-0851 β VERDICT
Verdict
NOT REPRODUCED at runtime β the bug is a real code-level defect (stale pointer dereference confirmed by source trace) but the race window is too narrow to trigger deterministically. The fix eliminates the stale read by construction and is validated (compiles, boots, PoC runs cleanly).
Finding
File: sys/vfs/isofs/cd9660/cd9660_vfsops.c, function iso_mountfs(), lines 407β466
Severity: Low
Title: Stale dereference of rootp after pribp released β use-after-free style read on buffer cache recycle
Mechanism (code-level trace)
The bug exists in the source code, confirmed line-by-line:
-
Line 329β347:
bread(devvp, ...)reads the ISO Primary Volume Descriptor (PVD) into buffer cache bufferpribp.pri = (struct iso_primary_descriptor *)vdpwherevdp = (struct iso_volume_descriptor *)pribp->b_data(line 333). Sopripoints intopribp->b_data. -
Line 407β410:
rootp = (struct iso_directory_record *)pri->root_directory_recordβrootpis derived frompri, which points intopribp->b_data.rootpis a pointer into the buffer cache data ofpribp. -
Line 428β430: Valid reads of
rootpwhilepribpis still held: -bcopy(rootp, isomp->root, sizeof isomp->root)β copies the root dir record -isomp->root_extent = isonum_733(rootp->extent)β reads extent field -isomp->root_size = isonum_733(rootp->size)β reads size field -
Line 435β437: The buffer is released:
c pribp->b_flags |= B_AGE; /* mark for preferential eviction */ brelse(pribp); /* release to buffer cache free list */ pribp = NULL;After this,rootpis a stale pointer β it still points into the released buffer's data area, which is now eligible for eviction and reuse. -
Line 466: Stale dereference:
c if ((error = bread(isomp->im_devvp, lblktooff(isomp, isomp->root_extent + isonum_711(rootp->ext_attr_length)), isomp->logical_block_size, &bp)) != 0)isonum_711(rootp->ext_attr_length)reads fromrootp, which points into the released buffer. If the buffer has been evicted and reused by a concurrentbread()for a different block, this reads stale/wrong data.
Why the race does not trigger at runtime
The race window is between brelse(pribp) (line 436) and isonum_711(rootp->ext_attr_length) (line 466). Examining lines 437β465: the code only performs field assignments (isomp->*, mp->*, dev->*, argp->*) β no I/O operations. Only a concurrent bread()/geteblk() from another CPU could evict the B_AGE-marked buffer in that window. The window is microseconds wide.
Testing with 3000 mount/unmount cycles and 8 concurrent I/O-pressure processes (heavy random reads from /boot/kernel/kernel, /var/log/messages, etc.) produced 0 failures on both unpatched and patched kernels. The buffer is virtually always still in cache when rootp->ext_attr_length is read.
Impact ceiling
- Worst case: If the race triggered,
rootp->ext_attr_lengthwould return a wrong byte βisomp->root_extent + wrong_byteβbread()reads the wrong block β either EIO (mount fails β DoS) or wrong RRIP detection (mount succeeds with wrong Rock Ridge behavior β correctness bug). - NOT memory corruption: The read is from valid buffer-cache memory (the buffer is on the free list, not freed to the slab allocator). It's a stale-data read, not a wild pointer dereference. No UAF primitive, no escalation chain possible.
- Reachability:
cd9660mount requiresSYSCAP_RESTRICTEDROOT(sys/kern/vfs_syscalls.c:5397), so only root can reachiso_mountfs(). The threat model is root mounting an attacker-provided ISO image under heavy concurrent I/O. No privilege boundary is crossed.
Reachability analysis
sys/kern/vfs_syscalls.c:152 priv = get_fscap(fstypename) sys/kern/vfs_syscalls.c:5397 get_fscap() returns SYSCAP_RESTRICTEDROOT for "cd9660" sys/kern/vfs_syscalls.c:154-158 caps_priv_check_td() β only root passes RESTRICTEDROOT
Unprivileged users cannot mount cd9660 even with vfs.usermount=1 (confirmed on guest: mount -t cd9660 as maxx β "Operation not permitted"). The bug is root-only reachable.
Fix
The fix caches ext_attr_length in a local variable before brelse(pribp), then uses the local at line 466:
+ int root_ext_attr_length; // new local variable
isomp->root_extent = isonum_733 (rootp->extent);
isomp->root_size = isonum_733 (rootp->size);
+ root_ext_attr_length = isonum_711(rootp->ext_attr_length); // cache BEFORE brelse
pribp->b_flags |= B_AGE;
brelse(pribp); // rootp is now stale, but root_ext_attr_length is safe
pribp = NULL;
...
- isonum_711(rootp->ext_attr_length) // STALE READ (eliminated)
+ root_ext_attr_length // uses cached value (safe)
This eliminates the stale read by construction. The full git-apply-able diff is in fix.diff.
Fix validation (Phase 8)
| Kernel | kern.version | PoC result |
|---|---|---|
| Unpatched baseline | 6.5-DEVELOPMENT #0 (Thu Jul 2 06:02:54 UTC 2026) | 3000 mounts ok, 0 failures |
| Single-fix kernel | 6.5-DEVELOPMENT #1 (Tue Jul 14 02:42:17 UTC 2026) | 3000 mounts ok, 0 failures |
The runtime behavior is identical because the race does not trigger on either kernel. The fix is validated by construction: the stale read at the former line 466 is eliminated β the code now reads from a stack local that was populated before brelse(). The patched kernel compiles cleanly (rc=0), boots, and the PoC runs without panics or new failures.
Fix status: fixed β the code-level defect is eliminated, the patched kernel works correctly.
Fix verification
fixedVALIDATED by construction: stale read eliminated (root_ext_attr_length cached before brelse). Both kernels identical behavior (race not triggerable). Compiles+boots.
Both #0 and #1: 3000 ok 0 failed. Stale read eliminated by construction.
Confirmed kernel references
Detail
Exploit chain
none -- stale read from valid buffer-cache memory (not slab-freed). Correctness/DoS only. Root-only (SYSCAP_RESTRICTEDROOT). No write primitive.
Evidence (decisive lines)
Both #0 and #1: 3000 ok, 0 failed. Code trace: rootp=pri->root_directory_record at :407, brelse at :436, isonum_711(rootp->ext_attr_length) at :466.
PoC changes
Authored from scratch: poc.c (3000 ISO mount loop + 8 I/O pressure children), run.sh (makefs ISO + vnconfig + mount loop), fix.diff (cache ext_attr_length before brelse), VERDICT.md, manifest.json.
Verified recommended fix
Cache isonum_711(rootp->ext_attr_length) in stack local at :430 (before brelse at :436); use it at :466 instead of stale rootp deref. Full git-apply-able diff in findings/poc/DF-0851/fix.diff.
Verdict
NOT REPRODUCED -- REAL code-level defect confirmed by trace but race too narrow. rootp derived from pribp->b_data at :407, brelse(pribp) at :436, stale deref rootp->ext_attr_length at :466. Window contains only field assignments (no I/O). 3000 mount cycles + I/O pressure: 0 failures on both kernels.
No comments yet.