β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0851

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0851 Β· 12 files
FileTypeDescriptionSize
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
README.md readme bug mechanism, build/run/expected, reachability note
↓ download 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_length returns a wrong byte β†’ isomp->root_extent + wrong_byte is 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) and isonum_711(rootp->ext_attr_length) (line 466), the code only does field assignments β€” no I/O. Only concurrent bread()/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_length before brelse. Behavior is identical at runtime (the race didn't trigger anyway), but the code is now correct by construction.
VERDICT.md verdict full analysis: code-level trace, why race doesn't trigger, impact ceiling, fix
↓ download raw

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:

  1. Line 329–347: bread(devvp, ...) reads the ISO Primary Volume Descriptor (PVD) into buffer cache buffer pribp. pri = (struct iso_primary_descriptor *)vdp where vdp = (struct iso_volume_descriptor *)pribp->b_data (line 333). So pri points into pribp->b_data.

  2. Line 407–410: rootp = (struct iso_directory_record *)pri->root_directory_record β€” rootp is derived from pri, which points into pribp->b_data. rootp is a pointer into the buffer cache data of pribp.

  3. Line 428–430: Valid reads of rootp while pribp is 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

  4. 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, rootp is a stale pointer β€” it still points into the released buffer's data area, which is now eligible for eviction and reuse.

  5. 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 from rootp, which points into the released buffer. If the buffer has been evicted and reused by a concurrent bread() 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_length would 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: cd9660 mount requires SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c:5397), so only root can reach iso_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

fixed
baseline no→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 02:42:17 UTC 2026

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.