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

cd9660_lookup evaluates shared dp->i_ino after vn_unlock in the DOTDOT path: concurrent lookup race can bind '..' to an arbitrary concurrently-scanned inode (wrong parent vnode) or spuriously fail

Summary

In the CNP_ISDOTDOT branch, the directory vnode is unlocked (vn_unlock(pdp), :343) and only afterwards are the arguments cd9660_vget_internal(vdp->v_mount, dp->i_ino, &tdp, dp->i_ino != ino, ep) evaluated (:344-345). dp->i_ino is a field of the shared iso_node that every concurrent cd9660_lookup on the same directory mutates continuously during its scan (:260 dp->i_ino = ino; :261 rrip_getname may overwrite via CL/PL inump; :273). A thread that wins the lock in the unlock-to-argument-evaluation window can swap dp->i_ino under the first thread, so '..' resolves via vget to an arbitrary entry's inode - a wrong-but-valid vnode (possibly a VREG returned as the parent directory, on which namei will run directory lookups), or a CL-garbage ino that merely errors. No memory corruption (vget_internal's relocated path bounds-checked; out-of-device inos fail EIO). Identical idiom in ufs_lookup.c:479-480/526-527 - fix should be coordinated. Trigger: two unpriv threads looping lstat('..') and stat('<other-entry>') in the same directory on any mounted ISO (RRIP images best groom). VFS-logic integrity issue, not privesc. Phase V skipped per contract (Low, instruction-scale window); certain by inspection. Fix: snapshot dp->i_ino/reloc into locals before vn_unlock. [VERIFIED pass2: 32.2M lstat dotdot iterations zero misresolutions - lock-handoff latency beats instruction-scale window; structurally real; fix compile-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3206 Β· 11 files
FileTypeDescriptionSize
craft_dotdot_iso.py β€” 3.4 KB view raw
racer.c β€” 2.8 KB view raw
run.log β€” 426 B view raw
README.md β€” 636 B ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
build.sh β€” 183 B view raw
run.sh β€” 157 B view raw
manifest.json β€” 730 B view raw
verdict.json β€” 1.8 KB view raw
fix.diff β€” 657 B view raw
env.txt β€” 272 B view raw

DF-3206 -- verification pack

Finding: cd9660_lookup evaluates shared dp->i_ino after vn_unlock in the DOTDOT path

Status: not_reproduced / impact denial_of_service / confidence likely

What was run

host: python3 craft_dotdot_iso.py out.iso; guest: cc -O -o racer racer.c -lpthread

How to reproduce

guest: mount_cd9660 -o ro /dev/vn2 /mnt && su -m nobody -c /tmp/racer /mnt 90

Expected

expected occasional HIT lines (ino != root); observed zero in 32.2M iterations

See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: craft_dotdot_iso.py, racer.c, run.log.

VERDICT.md
↓ download raw

DF-3206 verification

Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Bottom line

32.2 million lstat(DIR/..) iterations against a crafted RRIP ISO (64 sibling entries in the raced directory) with two concurrent scanners produced zero misresolutions in 90 seconds. The defect is structurally real - dp->i_ino is shared scan scratch, cd9660_lookup unlocks the directory at :343 and only then evaluates dp->i_ino as the vget argument at :344-345 - but the window between the unlock return and the i_ino load is a few instructions, and a blocked scanner wakes from the vnode lock far too late (sleep-based handoff) to overwrite i_ino inside it on this guest. The crafted ISO and racer are retained; the fix (snapshot i_ino/reloc into locals before the unlock) is compile-validated.

Kernel references

Exploit chain

(none: availability/correctness finding; no privilege chain)

Fix

snapshot dp->i_ino and the reloc flag into locals before vn_unlock

See fix.diff in this directory (git-apply-able against the read-only sys/ tree; applied only inside the disposable verification guest).

Reproduction

build: host: python3 craft_dotdot_iso.py out.iso; guest: cc -O -o racer racer.c -lpthread

run: guest: mount_cd9660 -o ro /dev/vn2 /mnt && su -m nobody -c /tmp/racer /mnt 90

expected: expected occasional HIT lines (ino != root); observed zero in 32.2M iterations

Notes on the PoC (what changed vs the finding sketch)

namecache serves .. after first resolution, so hit rate depends on cache churn (vfs.cache clean_neg_count 9M shows aggressive reclaim); 3 racing threads give 2 real scanners

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

not_testable
↓ fix.diffper-fix-DF-3206

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: root ino 65536, DIR ino 69632, a_iters=32228978 a_hits=0; racer.c, craft_dotdot_iso.py

PoC changes

namecache serves .. after first resolution, so hit rate depends on cache churn (vfs.cache clean_neg_count 9M shows aggressive reclaim); 3 racing threads give 2 real scanners

Verified recommended fix

snapshot dp->i_ino and the reloc flag into locals before vn_unlock

Verdict

32.2 million lstat(DIR/..) iterations against a crafted RRIP ISO (64 sibling entries in the raced directory) with two concurrent scanners produced zero misresolutions in 90 seconds. The defect is structurally real - dp->i_ino is shared scan scratch, cd9660_lookup unlocks the directory at :343 and only then evaluates dp->i_ino as the vget argument at :344-345 - but the window between the unlock return and the i_ino load is a few instructions, and a blocked scanner wakes from the vnode lock far too late (sleep-based handoff) to overwrite i_ino inside it on this guest. The crafted ISO and racer are retained; the fix (snapshot i_ino/reloc into locals before the unlock) is compile-validated.