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

UFS inode-hash has no lock; concurrent ffs_vget() inserts orphan an inode and panic on reclaim (dual-vnode on production)

Field Value
ID DF-0928
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H
CWE CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization
File sys/vfs/ufs/ufs_ihash.c
Lines 78-189
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

Every walk/insert/remove of the per-mount UFS inode hash chain in sys/vfs/ufs/ufs_ihash.c runs with no synchronization primitive whatsoever β€” there is no mutex, no lwkt_token, no vnode-held requirement on entry. The otherwise-identical sibling sys/vfs/ext2fs/ext2_ihash.c wraps each of these operations in a static lwkt_token (ext2_ihash.c:53,94,131,156) and explicitly documents that it exists to serialize the chain. Because ufs_ihashins's duplicate-detection scan (ufs_ihash.c:156-161) and the actual *ipp = ip store (ufs_ihash.c:163) are not atomic w.r.t. each other, two concurrent ffs_vget() calls for the same (dev,ino) can both observe an empty bucket and both write the bucket head, orphaning the first writer's inode (IN_HASHED set but unreachable). On INVARIANTS kernels the orphan trips KKASSERT(ip == iq) at ufs_ihash.c:184 during ufs_reclaim β†’ panic; on production kernels it produces two live vnodes for one on-disk inode with divergent cached metadata (cache incoherence, stale mode/permissions).

Root cause

No synchronization protects ump->um_ihashtbl or the inode.i_next chain.

(a) ufs_ihashins (ufs_ihash.c:148-166): the duplicate scan

while ((iq = *ipp) != NULL) {
    if (ip->i_dev == iq->i_dev && ip->i_number == iq->i_number) {
        return(EBUSY);
    }
    ipp = &iq->i_next;
}
ip->i_next = NULL;
*ipp = ip;                 /* <-- racing store */
ip->i_flag |= IN_HASHED;

is a classic check-then-act TOCTOU. Two threads A and B in ffs_vget (ffs_vfsops.c:1066) for the same ino both miss in ufs_ihashget (ffs_vfsops.c:1081), both getnewvnode + kmalloc an inode, and both call ufs_ihashins. If both read *ipp == NULL for the bucket before either executes *ipp = ip, the second store silently overwrites the first; thread A's inode has i_flag |= IN_HASHED (ufs_ihash.c:164) but is not reachable through the chain.

The EBUSY retry in ffs_vfsops.c:1120-1128 only catches the detected case (B sees A's entry); it does nothing for the undetected dual-store.

(b) ufs_ihashrem (ufs_ihash.c:171-189): later walks the chain for thread A's orphan, fails to find it, and fires KKASSERT(ip == iq) at ufs_ihash.c:184 (iq is NULL).

(c) Walkers (ufs_ihashget, ufs_ihashlookup, ufs_ihashcheck): all dereference ip->i_number, ip->i_dev, and ip->i_next from the chain with no lock and no reference on the inode; a concurrent ufs_reclaim (ufs_inode.c:108-165) which runs ufs_ihashrem (ufs_inode.c:145) and then kfree(ip) (ufs_inode.c:162) leaves a narrow use-after-free-read window on the walked inode.

Contrast with the correct sibling implementation sys/vfs/ext2fs/ext2_ihash.c which declares static struct lwkt_token ext2_ihash_token (ext2_ihash.c:53) and acquires it in ext2_ihashget (:94,114,117), ext2_ihashins (:131,135,143), and ext2_ihashrem (:156,169) β€” ufs_ihash.c is missing every one of those calls.

Threat model & preconditions

  • Attacker position: Local, unprivileged.
  • Privileges gained or impact: 1. INVARIANTS/Debug kernel: reliable local DoS via KKASSERT panic in ufs_ihashrem when the orphan is reclaimed. 2. Production (non-INVARIANTS) kernel: cache incoherence β€” two independent struct inode/vnode copies of the same on-disk inode. Metadata changes (chmod/chown/setuid, link count, size) made through one vnode are invisible to lookups going through the hash β†’ stale permission checks / setuid-bit retention / divergent ffs_update writes (I:L). 3. NFS-exported UFS: remote variant. nfs_serv.c builds READDIRPLUS replies by calling VFS_VGET once per directory entry (nfs_serv.c:3417,3454), so two concurrent NFS clients (or one multi-threaded client) issuing READDIRPLUS/LOOKUP for overlapping entries drive concurrent ffs_vget β†’ concurrent ufs_ihashins for the same dp->d_ino. An attacker controlling an NFS client does not need credentials beyond what the export already grants.
  • Required config or capabilities: A UFS (FFS/MFS) filesystem; for the remote vector, nfsd running and the filesystem exported.
  • Reachability: Spawn N threads that, after a barrier, all open() the same file whose vnode has just been evicted from the cache (eviction forced by thrashing the vnode cache, or simply racing the very first lookup of a freshly-created file). All N threads miss in ufs_ihashget and race into ufs_ihashins.

Proof of concept

PoC source: findings/poc/DF-0928/race_ufs_ihash.c

Build & run

cc -O2 -pthread -o race_ufs_ihash race_ufs_ihash.c
./race_ufs_ihash        # on a UFS-mounted /tmp

Expected output

On an INVARIANTS kernel:

Kernel panic: ufs_ihashrem
...
ufs_ihashrem(...) at ufs_ihashrem+0x...   (ufs_ihash.c:184 KKASSERT)
ufs_reclaim(...)  at ufs_reclaim+0x...
vclean(...)       at vclean+0x...
...

On a production kernel, observe two vnodes for the same (dev, ino) via fstat/vfsaudit, or run a side-channel check that chmod(04755)-then-chmod(0644) on the racing threads leaves a vnode still reporting the setuid bit.

For the remote variant: export /srv via NFS, then from two client hosts run find /mnt -exec true {} + (READDIRPLUS-heavy) in a tight loop; the server eventually panics the same way.

Impact

  • INVARIANTS kernel: reliable local DoS (panic) β€” and a remote DoS on NFS-exported UFS.
  • Production kernel: cache-incoherence β†’ stale permission/setuid checks on a file that an attacker can race on; integrity violation.

Add a per-mount lwkt_token mirroring the proven ext2_ihash.c pattern, and acquire it around every chain walk/insert/remove. The token is held across the blocking vget() in ufs_ihashget exactly as ext2_ihashget holds it across its vget β€” this is the established DragonFly idiom and makes the check-then-act in ufs_ihashins atomic w.r.t. other inserters/removers.

--- a/sys/vfs/ufs/ufsmount.h
+++ b/sys/vfs/ufs/ufsmount.h
@@ -84,6 +84,8 @@ struct ufsmount {
    struct malloc_type *um_malloctype;  /* The inodes malloctype */
    int um_i_effnlink_valid;        /* i_effnlink valid? */
    struct inode **um_ihashtbl;     /* inum to inode map */
    u_long  um_ihash;           /* size of hash table - 1 */
+   struct lwkt_token um_ihash_token;   /* guards um_ihashtbl + i_next */
 };

--- a/sys/vfs/ufs/ufs_ihash.c
+++ b/sys/vfs/ufs/ufs_ihash.c
@@ -33,6 +33,7 @@
 #include <sys/param.h>
 #include <sys/systm.h>
 #include <sys/kernel.h>
+#include <sys/thread.h>
 #include <sys/lock.h>
 #include <sys/vnode.h>
 #include <sys/malloc.h>
@@ -55,12 +56,17 @@ static MALLOC_DEFINE(M_UFSIHASH, "UFS ihash", "UFS Inode hash tables");
 void
 ufs_ihashinit(struct ufsmount *ump)
 {
+   lwkt_token_init(&ump->um_ihash_token, "ufsihash");
+   lwkt_gettoken(&ump->um_ihash_token);
    ump->um_ihash = vfs_inodehashsize();
    ump->um_ihashtbl = kmalloc(sizeof(void *) * ump->um_ihash,
                   M_UFSIHASH,
                   M_WAITOK|M_ZERO);
    --ump->um_ihash;
+   lwkt_reltoken(&ump->um_ihash_token);
 }

 void
 ufs_ihashuninit(struct ufsmount *ump)
 {
+   lwkt_gettoken(&ump->um_ihash_token);
    if (ump->um_ihashtbl) {
        kfree(ump->um_ihashtbl, M_UFSIHASH);
        ump->um_ihashtbl = NULL;
    }
+   lwkt_reltoken(&ump->um_ihash_token);
 }
@@ -148,6 +154,8 @@ int
 ufs_ihashins(struct ufsmount *ump, struct inode *ip)
 {
    struct inode **ipp;
    struct inode *iq;

+   lwkt_gettoken(&ump->um_ihash_token);
    KKASSERT((ip->i_flag & IN_HASHED) == 0);
    ipp = INOHASH(ump, ip->i_number);
    while ((iq = *ipp) != NULL) {
        if (ip->i_dev == iq->i_dev && ip->i_number == iq->i_number) {
+           lwkt_reltoken(&ump->um_ihash_token);
            return(EBUSY);
        }
        ipp = &iq->i_next;
    }
    ip->i_next = NULL;
    *ipp = ip;
    ip->i_flag |= IN_HASHED;
+   lwkt_reltoken(&ump->um_ihash_token);
    return(0);
 }
@@ -171,12 +179,15 @@ void
 ufs_ihashrem(struct ufsmount *ump, struct inode *ip)
 {
    struct inode **ipp;
    struct inode *iq;

+   lwkt_gettoken(&ump->um_ihash_token);
    if (ip->i_flag & IN_HASHED) {
        ipp = INOHASH(ump, ip->i_number);
        while ((iq = *ipp) != NULL) {
            if (ip == iq)
                break;
            ipp = &iq->i_next;
        }
        KKASSERT(ip == iq);
        *ipp = ip->i_next;
        ip->i_next = NULL;
        ip->i_flag &= ~IN_HASHED;
    }
+   lwkt_reltoken(&ump->um_ihash_token);
 }
@@ ufs_ihashget / ufs_ihashlookup / ufs_ihashcheck: take the token at entry,
                                              hold it across vget (in ufs_ihashget),
                                              release on every return path.

Because lwkt_token is held by the calling thread and the walker re-validates the chain after the blocking vget() (the existing re-walk at ufs_ihash.c:115-122), concurrent inserters/removers are now serialized: the second ufs_ihashins will observe the first's *ipp = ip store and correctly return EBUSY, the existing ffs_vget retry path handles it, and ufs_ihashrem's KKASSERT will no longer fire on a legitimately-orphaned inode because no orphan can be created. The narrower walk-UAF against concurrent ufs_reclaim is also closed because the token serializes ufs_ihashrem (which runs under ufs_reclaim) against every walker.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0928 Β· 12 files
FileTypeDescriptionSize
race_ufs_ihash.c trigger-source original reviewer-written PoC (targets /tmp, fixed to use UFS mount) 6.0 KB view raw
race_ufs_ihash2.c trigger-source improved PoC: hardlinks in separate dirs + churner threads 4.3 KB view raw
race_ufs_ihash3.c trigger-source max-aggression PoC: 16 racers + 8 churners + 500 files on tiny-hash UFS 4.2 KB view raw
build.sh build-script cc -O2 -pthread -o race_ufs_ihash3 race_ufs_ihash3.c 257 B view raw
run.sh run-script run race_ufs_ihash3 on /mnt/ufs_test 318 B view raw
VERDICT.md verdict full narrative: source-level confirmation, namecache serialization analysis, fix validation 6.6 KB ↓ raw
fix.diff suggested-fix git-apply-able: adds lwkt_token to all 7 ufs_ihash functions, mirroring ext2_ihash.c 3.0 KB view raw
fix_build.log build-log full nativekernel build output for fixed kernel (rc=0) 5.6 MB ↓ download
fix_run.log run-log PoC run on fixed kernel (no panic) 142 B view raw
README.md readme human reproduce doc 1.6 KB ↓ 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 human reproduce doc
↓ download raw

DF-0928 β€” PoC: UFS inode-hash dual-insert race

Goal

Force a dual ufs_ihashins store for the same (dev,ino) by racing N threads' open() calls on a file whose vnode has just been evicted from the cache. The loser inode is orphaned (IN_HASHED set but not on the chain). On INVARIANTS kernels the orphan trips KKASSERT(ip == iq) at ufs_ihash.c:184 when reclaimed; on production kernels it produces two vnodes for one inode with divergent cached metadata.

Build & run (DragonFlyBSD guest with UFS-mounted /tmp)

cc -O2 -pthread -o race_ufs_ihash race_ufs_ihash.c
./race_ufs_ihash

Optional (root): sysctl vfs.maxvnodes=256 to make reproduction near-deterministic.

Expected output

INVARIANTS kernel

Kernel panic: ufs_ihashrem
...
ufs_ihashrem(...) at ufs_ihashrem+0x...   (ufs_ihash.c:184 KKASSERT)
ufs_reclaim(...)  at ufs_reclaim+0x...
vclean(...)       at vclean+0x...
vrecycle(...)     at vrecycle+0x...
...

Production (non-INVARIANTS) kernel

No panic; instead observe two vnodes for the same (dev, ino) via fstat | awk '$2 == <ino>', or run a side-channel check that chmod(04755)-then-chmod(0644) from racing threads leaves a vnode still reporting the setuid bit.

Notes

  • The remote variant: export /srv via NFS, then from two client hosts run find /mnt -exec true {} + (READDIRPLUS-heavy) in a tight loop; the server eventually panics the same way.
  • This is the same concurrency bug class as the FFS-era inode-hash races fixed in FreeBSD/NetBSD years ago; DragonFlyBSD's ext2_ihash.c has the fix (a lwkt_token), ufs_ihash.c does not.
VERDICT.md verdict full narrative: source-level confirmation, namecache serialization analysis, fix validation
↓ download raw

DF-0928 β€” VERDICT

Verdict: REPRODUCED AT SOURCE LEVEL β€” missing lock confirmed; race not triggered from local userspace

The bug described in DF-0928 is real and confirmed at the source level: the UFS/FFS inode hash (sys/vfs/ufs/ufs_ihash.c) has zero synchronization protecting the hash table or the inode.i_next chain. Every function β€” ufs_ihashinit, ufs_ihashuninit, ufs_ihashget, ufs_ihashlookup, ufs_ihashcheck, ufs_ihashins, ufs_ihashrem β€” walks or modifies the chain with no mutex, no lwkt_token, no held reference. The otherwise-identical sibling sys/vfs/ext2fs/ext2_ihash.c wraps every one of these operations in a static struct lwkt_token ext2_ihash_token (ext2_ihash.c:53).

However, despite extensive testing (multiple PoC variants, 5+ minutes of aggressive racing, a delay-injected kernel that widened the TOCTOU window to 5ms inside ufs_ihashins), no panic was triggered from local userspace. The root cause is that the DragonFlyBSD namecache provides incidental serialization that prevents concurrent ufs_iget/ufs_ihashins calls for the same inode from local processes.

Mechanism (confirmed by source analysis)

The missing lock (FACT)

ufs_ihash.c (entire 190-line file read) contains not a single synchronization primitive. Compare:

Function ufs_ihash.c ext2_ihash.c (sibling)
*_init no lock lwkt_token_init (:67)
*_uninit no lock lwkt_gettoken/reltoken (:73,76)
*_get no lock lwkt_gettoken/reltoken (:94,114,117)
*_ins no lock lwkt_gettoken/reltoken (:131,135,143)
*_rem no lock lwkt_gettoken/reltoken (:156,169)

The TOCTOU in ufs_ihashins (ufs_ihash.c:154-164)

// SCAN: check for existing entry (no lock held)
while ((iq = *ipp) != NULL) {
    if (ip->i_dev == iq->i_dev && ip->i_number == iq->i_number)
        return(EBUSY);
    ipp = &iq->i_next;
}
// *** RACE WINDOW: another thread can scan+store here ***
ip->i_next = NULL;
*ipp = ip;              // STORE: overwrites any concurrent store
ip->i_flag |= IN_HASHED;

Two concurrent ffs_vget() calls for the same (dev,ino) can both scan the empty bucket, both see no duplicate, and both store β€” the second store silently overwrites the first, orphaning the loser's inode (IN_HASHED set but not on the chain). On INVARIANTS kernels, the orphan trips KKASSERT(ip == iq) at ufs_ihash.c:184 during ufs_reclaim β†’ ufs_ihashrem.

The walker-UAF (ufs_ihash.c:105, 138, 179)

All walker functions dereference ip->i_number, ip->i_dev, ip->i_next from the chain with no lock and no reference on the walked inode. A concurrent ufs_reclaim (ufs_inode.c:145 β†’ ufs_ihashrem) β†’ kfree(ip) (ufs_inode.c:162) leaves a use-after-free-read window.

Why the race was NOT triggered from local userspace

Instrumented-kernel testing (kprintf + 5ms DELAY inside the TOCTOU window of ufs_ihashins) confirmed:

  1. ufs_ihashins IS called β€” 1139 calls observed during an 8-second run, from multiple CPUs (cpu=0, cpu=1, cpu=5).
  2. ihashins collision (EBUSY) count = 0 β€” no two threads ever called ufs_ihashins for the same inode concurrently.
  3. Root cause: the DragonFlyBSD namecache provides incidental serialization. When any thread resolves ANY path to an inode and calls ufs_ihashins, the vnode is created and inserted into the hash. All subsequent lookups via ANY path (including different hardlink paths in separate directories) call ufs_ihashget β†’ hit β†’ return the existing vnode. No second ufs_ihashins call occurs for the same inode.

The dual-insert race window requires the inode's vnode to have been fully reclaimed (removed from hash by ufs_ihashrem) AND two lookups to both miss ufs_ihashget before either's ufs_ihashins stores. The namecache resolves the first lookup within microseconds, closing the window before a second lookup can enter.

The realistic trigger: NFS READDIRPLUS

The finding's NFS variant (nfs_serv.c builds READDIRPLUS replies by calling VFS_VGET once per directory entry, nfs_serv.c:3417,3454) bypasses the namecache and drives concurrent ffs_vget for overlapping entries directly. Two NFS clients issuing concurrent READDIRPLUS for the same directory would race. This was not tested (no NFS server setup on this guest).

Exploit chain assessment

This is a race condition (CWE-362), not a deterministic memory-corruption primitive. There is no reliable write/control primitive to escalate. On an INVARIANTS kernel (default GENERIC), the impact is DoS (panic). On a production (non-INVARIANTS) kernel, the impact would be cache incoherence (two vnodes for one inode β†’ stale permissions). No uid=0 escalation path was identified.

Fix

Authored fix.diff: adds a per-mount struct lwkt_token um_ihash_token to struct ufsmount (ufsmount.h) and acquires/releases it in all 7 functions (ufs_ihashinit, ufs_ihashuninit, ufs_ihashget, ufs_ihashlookup, ufs_ihashcheck, ufs_ihashins, ufs_ihashrem). This exactly mirrors the proven ext2_ihash.c pattern.

Fix validation

  • Applies: git apply --check passes cleanly (133-line diff, 2 files).
  • Compiles: make -j6 nativekernel rc=0 (full build log in fix_build.log).
  • Boots: fixed kernel #1 boots and runs normally.
  • PoC on fixed kernel: runs 50s without panic (same as unpatched β€” the race can't be triggered from local userspace on either kernel due to namecache serialization).
  • Behavioral before/after: not_testable β€” the PoC cannot trigger the race on either kernel. The fix is validated at the code level: all 7 hash operations are now serialized by the token, matching the proven ext2fs sibling.

PoC files

  • race_ufs_ihash.c β€” original PoC (reviewer-written, operates on /tmp which is tmpfs on this guest β€” corrected to use a UFS mount).
  • race_ufs_ihash2.c β€” improved PoC using hardlinks in separate directories.
  • race_ufs_ihash3.c β€” maximum-aggression PoC: 16 racers + 8 churners + 500 pre-created files on a tiny-hash UFS mount.

PoC changes

  1. Changed target from /tmp (tmpfs on this guest β€” does NOT exercise UFS inode hash) to a UFS mount (/mnt/ufs_test, created via vnconfig + newfs).
  2. Added hardlink-based racing across separate directories to bypass the parent-directory vnode lock that serializes same-directory lookups.
  3. Added churner threads (create+delete files) to maintain vnode recycling pressure.
  4. Created two additional PoC variants (race_ufs_ihash2.c, race_ufs_ihash3.c) with progressively more aggressive racing strategies.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

NOT TESTABLE behaviorally: the PoC cannot trigger the race on either the unpatched baseline or the single-fix kernel because the namecache provides incidental serialization. The fix was validated at the code level: git apply --check passes, make -j6 nativekernel rc=0, the fixed kernel #1 boots and runs the PoC without issue, all 7 hash operations are now serialized by lwkt_token matching ext2_ihash.c.

Baseline (#0): PoC ran 150s on UFS mount, no panic, guest up. Delay-injected (#1, 5ms DELAY): 50s, no panic, 1139 ihashins calls, 0 EBUSY collisions. Fixed kernel (#1, lwkt_token): 50s, no panic, guest up. All three kernels exhibit identical behavior because the namecache prevents the race from local userspace.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 08:25:40 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

Race condition (CWE-362), not a deterministic memory-corruption primitive. The dual-insert TOCTOU could theoretically orphan an inode. The walker-UAF could theoretically read poisoned memory. Neither was triggered from local userspace because the namecache serializes concurrent VFS_VGET calls for the same inode. No uid0 escalation path exists -- this is a DoS/cache-incoherence bug. The finding's threat model (NFS READDIRPLUS) is the realistic trigger but was not testable on this guest.

Evidence (decisive lines)

Source-level (definitive): ufs_ihash.c has 0 lock acquisitions in all 190 lines; ext2_ihash.c has 11 lwkt_gettoken/reltoken calls. Instrumented kernel: 1139 ufs_ihashins calls from cpu=0,1,5 but 'ihashins collision' (EBUSY) count=0 -- no concurrent same-inode inserts. Delay-injected kernel (5ms DELAY inside TOCTOU): also no panic after 50s of aggressive racing.

PoC changes

Fixed target FS (original PoC used /tmp which is tmpfs); added UFS mount setup via vnconfig+newfs. Created 3 PoC variants: hardlinks across separate dirs, 16 racers + 8 churners + 500 pre-created files, delay-injected kernel. Authored fix.diff (lwkt_token in all 7 ufs_ihash functions, matching ext2_ihash.c).

Verified recommended fix

Add a per-mount struct lwkt_token um_ihash_token to struct ufsmount (ufsmount.h:88) and acquire/release it in all 7 ufs_ihash functions, exactly mirroring the proven ext2_ihash.c pattern. The token is held across the blocking vget() in ufs_ihashget (as ext2_ihashget does at ext2_ihash.c:94,114). This closes the TOCTOU in ufs_ihashins and serializes walkers against ufs_ihashrem. Matches finding proposal; extends coverage to ufs_ihashlookup and ufs_ihashcheck. Full git-apply-able diff in findings/poc/DF-0928/fix.diff.

Verdict

NOT REPRODUCED at runtime, but the code-level defect is CONFIRMED REAL. The UFS inode hash (sys/vfs/ufs/ufs_ihash.c, entire 190-line file) has ZERO synchronization -- no mutex, no lwkt_token, no held-reference requirement on any of its 7 functions. The sibling ext2_ihash.c correctly wraps every operation in lwkt_token ext2_ihash_token. The TOCTOU in ufs_ihashins (scan at :156-161, store at :163, no atomicity) and the KKASSERT(ip==iq) at ufs_ihash.c:184 are both real. However, despite extensive testing (3 PoC variants, 5+ minutes of aggressive racing, delay-injected kernel widening TOCTOU to 5ms), NO PANIC was triggered. ROOT CAUSE: the DragonFlyBSD namecache provides incidental serialization -- once any thread resolves ANY path to an inode, the vnode is in the hash, and ALL subsequent lookups hit ufs_ihashget. The realistic trigger is NFS READDIRPLUS which bypasses the namecache -- not testable on this guest.