Page MenuHomeFreeBSD

vfs: handle file mounts in vn_fullpath_hardlink()
AbandonedPublic

Authored by nick_spun.io on Jul 11 2026, 11:22 PM.
Tags
None
Referenced Files
F168275031: D58178.id.diff
Thu, Aug 27, 8:18 AM
Unknown Object (File)
Wed, Aug 26, 3:11 AM
Unknown Object (File)
Wed, Aug 26, 12:49 AM
Unknown Object (File)
Tue, Aug 25, 11:57 PM
Unknown Object (File)
Tue, Aug 25, 11:31 PM
Unknown Object (File)
Thu, Aug 20, 7:04 PM
Unknown Object (File)
Fri, Aug 14, 7:47 PM
Unknown Object (File)
Thu, Aug 13, 2:15 PM
Subscribers

Details

Reviewers
kib
markj
olce
Summary

For file mounts lookup has no directory to return as ni_dvp: the mount
root is a VREG vnode with VV_ROOT set. It used to return vp_crossmp;
since 29d1a3248a6d it returns the covered vnode, which for a file mount
is itself a regular file. Either way ni_dvp is not a directory, and
execve(2) stores it as p_textdvp. vn_fullpath_hardlink() appends the
saved name and restarts the walk from p_textdvp assuming it is a
directory, so a later kern.proc.pathname query either

  • takes the lock-free namecache reverse walk, which does not check the type and so "succeeds" on the covered file, returning a bogus doubled path -- a relative execve(2) of a file mount reports .../cover/cover in AT_EXECPATH on today's main; or
  • falls back to vn_fullpath_dir() once the covered file has no namecache entry, tripping VNPASS(vp->v_type == VDIR || VN_IS_DOOMED(vp)) on INVARIANTS kernels.

That is PR 282596: www/foreign-cdm execs its Widevine worker from a
single-file nullfs mount, and htop or procstat -b against that process
panics the box once the entry has been evicted under namecache pressure.

Resolve file mounts in vn_fullpath_hardlink() itself via the covered
vnode, the same way a1d74b2dab78 handled realpath(2); the resulting
path is unambiguous since file mounts require a link count of 1. This
covers all callers: kern.proc.pathname, AT_EXECPATH for a relative
execve of a file mount, and vn_path_to_global_path_hardlink(). Drop
the now-redundant special case in kern___realpathat().

The pre-lock VV_ROOT test is made without the vnode lock and is only
a hint: recheck it once the lock is held, before dereferencing
v_mount, and return ENOENT when it no longer holds. That matches
vn_lock(), which returns ENOENT when the vnode is reclaimed during
the same window.

Since vn_fullpath() resolves into its own MAXPATHLEN buffer, check the
result against the caller-supplied size and return ENAMETOOLONG when
it does not fit, as the dropped special case did via the check in
kern___realpathat(); without this, __realpathat(2) would copy out past
the size given by the caller. Take the branch before the size
validation and work buffer allocation so that undersized buffers keep
returning ENAMETOOLONG rather than EINVAL and the work buffer is not
allocated just to be freed.

PR: 282596
Signed-off-by: Nick Price <nick@spun.io>

Test Plan

Both attached reproducers, on a GENERIC (INVARIANTS) amd64 bhyve VM:

main-n287675-95439b803fce      both reproducers panic
+ this revision (n287676)      no panic, mount path reported

panic: condition vp->v_type == VDIR || VN_IS_DOOMED(vp) not met
       at sys/kern/vfs_cache.c:3547 (vn_fullpath_dir)
vn_fullpath_dir()
vn_fullpath_hardlink()
proc_get_binpath()
sysctl_kern_proc_pathname()

which matches the backtrace in the textdump on PR 282596. With the
revision applied both reproducers report the mount path, e.g.
"pathname = /tmp/filemount.iWdI2g.renamed/cover".

filemount_panic.sh is the shell version; filemount_panic.c is the same
sequence as bare syscalls: nmount(2) of a single-file nullfs mount,
execve(2) of the mount point, rename(2) of the containing directory,
then kern.proc.pathname.

The rename(2) is what makes this still reproduce on today's main. It
drops the covered file's namecache entry via cache_vop_rename(), so the
reverse walk has to fall back to vn_fullpath_dir(). Without it the
lock-free walk silently returns the doubled path instead, which is why
the reproducers posted earlier no longer panicked after 29d1a3248a6d.

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 74748
Build 71631: arc lint + arc unit

Event Timeline

This happens more than once, as indicated in e.g. d53633bfcf24a3eb37. I wonder of we should fix lookup/namei for the case of regular mount, once and for all. See D58191 which is only a draft, which is not tested.

sys/kern/vfs_cache.c
3846

At least you need to recheck the condition after the vnode is locked, to see that it is really the root still.

In D58178#1334439, @kib wrote:

This happens more than once, as indicated in e.g. d53633bfcf24a3eb37. I wonder of we should fix lookup/namei for the case of regular mount, once and for all. See D58191 which is only a draft, which is not tested.

I think a structural fix is definitely the way to go otherwise these conditions will probably keep popping up - I'll run D58191 for a while and see if anything pops up

Been trying to figure out some structural fixes as well to prevent/mitigate assumptions that mount roots are directories (nullfs can mount files as well) but there doesn't seem to be a real "silver bullet" fix I can identify

I suspect that even after D58191 some fix along these lines is needed. The fullpath routines need to be prepared that the parent is not VREG.

Could you confirm this?

In D58178#1334901, @kib wrote:

I suspect that even after D58191 some fix along these lines is needed. The fullpath routines need to be prepared that the parent is not VREG.

Could you confirm this?

Reviewed everything and I think this is still gonna be needed

Could somebody provide an isolated test for the situation where the patch is needed?

The start would be to look at the syscall sequence that triggers the problem, with e.g. ktrace.

In D58178#1337559, @kib wrote:

Could somebody provide an isolated test for the situation where the patch is needed?

The start would be to look at the syscall sequence that triggers the problem, with e.g. ktrace.

Added some simpler repr attachments - note that we're only using /bin/sleep in this example because it's an easy binary that will stay running for a while

{F162961023}

{F162961021}

Confirmed today (by accidentally removing this patch from my local tree) that this is still required 😆️

Confirmed today (by accidentally removing this patch from my local tree) that this is still required 😆️

So can you provide the isolated test that fails on today main? @pho

Apologies, the repro cases I posted previously need to be reworked a bit

Here are two new clean reproducers for this panic - there is also another panic doing mount_nullfs -o mountdir $(mktemp) (using -o mountdir on top of a file) but that is outside of this scope.

nick_spun.io edited the test plan for this revision. (Show Details)

Here are two new clean reproducers for this panic - there is also another panic doing mount_nullfs -o mountdir $(mktemp) (using -o mountdir on top of a file) but that is outside of this scope.

I'm not seeing these attachment. Could you please email them to me?

In D58178#1341543, @pho wrote:

Here are two new clean reproducers for this panic - there is also another panic doing mount_nullfs -o mountdir $(mktemp) (using -o mountdir on top of a file) but that is outside of this scope.

I'm not seeing these attachment. Could you please email them to me?

Just sent, I think I fixed the permissions on Phabricator as well.

Adding this here as well just because it's a short repro

dir=$(mktemp -d)
: >"$dir/cover"                        # file to mount over, one hardlink
mount_nullfs /bin/sleep "$dir/cover"   # single-file nullfs mount
"$dir/cover" 300 &                     # execve() -> p_textdvp = covered file
sleep 1                                # let the exec finish
mv "$dir" "$dir.renamed"               # evict the cover file's cache entry
procstat -b $!                         # kern.proc.pathname -> panic

I no longer get a panic with the two test scenarios and D58178.id181841.diff added..

I think a more radical and simultaneously correct approach is D58506