git: 23b326728778 - main - namei: retry absolute symlink expansions from the native root
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Sun, 18 Oct 2026 15:48:52 UTC
The branch main has been updated by dteske:
URL: https://cgit.FreeBSD.org/src/commit/?id=23b326728778fd3a6c37b33686dea98d010eb1cf
commit 23b326728778fd3a6c37b33686dea98d010eb1cf
Author: Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-10-18 15:44:12 +0000
Commit: Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-10-18 15:44:12 +0000
namei: retry absolute symlink expansions from the native root
For a Linux ABI process, namei() resolves a path first under the ABI
root (compat.linux.emul_path) and, if that fails, restarts the walk
of the original path from the native root. Since 459ac3044b51 an
absolute symlink target found during the ABI pass stays under the
ABI root, so self-contained Linux userlands resolve their own
symlinks correctly (PR 289739); but when the target names a file
that exists only in the native namespace, the walk fails and the
restart cannot recover: it re-walks the original path, whose
ABI-visible prefix (the symlink itself) does not exist natively.
Casualties are the absolute symlinks planted under the emulator root
to integrate the host into the Linux namespace -- among them
linux-rl9-fontconfig's /compat/linux/etc/fonts ->
/usr/local/etc/fonts (Linux fontconfig cannot load its
configuration, degrading font matching to arbitrary results) and
similar links from linux-rl9-alsa-lib and linux-rl9-ca-certificates
-- and linprocfs' /proc/<pid>/exe, whose target is a host path:
following it fails for any Linux binary whose text lives outside the
emulator root (Chromium-based browsers execve /proc/self/exe to
spawn helpers), unless procfs(5) mounted at the native /proc masks
the failure.
Remember the expansion produced while following an absolute symlink
during the ABI pass and, when that pass fails with ENOENT before the
target has resolved, perform the one allowed native restart walking
the saved expansion in place of the original path. The target has
resolved once the walk has consumed it, leaving no more than the
suffix appended past it; an ENOENT after that point restarts from
the original path as it always has. Lookups that succeed under the
ABI root are unaffected, preserving 459ac3044b51; lockless lookups
are unaffected because cache_fplookup_symlink() already aborts to
the locked path for absolute symlinks whenever an ABI root is
active.
PR: 297426
Fixes: 459ac3044b51 ("namei: Preserve ABI root for absolute symlinks before fallback")
MFC after: 1 month
Reviewed by: kib
Differential Revision: https://reviews.freebsd.org/D58855
---
sys/kern/vfs_lookup.c | 106 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 105 insertions(+), 1 deletion(-)
diff --git a/sys/kern/vfs_lookup.c b/sys/kern/vfs_lookup.c
index 34e9bb47b532..a1d8a8cd765f 100644
--- a/sys/kern/vfs_lookup.c
+++ b/sys/kern/vfs_lookup.c
@@ -613,11 +613,25 @@ namei(struct nameidata *ndp)
struct componentname *cnp;
struct thread *td;
struct pwd *pwd;
+ char *abi_symlink; /* abs symlink expansion from the ABI pass */
+ size_t abi_symlink_len;
+ size_t abi_suffix_len; /* leftover path past the saved target */
+ bool target_resolved; /* walk has consumed that target */
+ enum {
+ NAMEI_ABI_PASS_ABI = 0,
+ NAMEI_ABI_PASS_SYMLINK,
+ NAMEI_ABI_PASS_NATIVE,
+ } abi_pass;
int error;
enum cache_fpl_status status;
cnp = &ndp->ni_cnd;
td = curthread;
+ abi_symlink = NULL;
+ abi_symlink_len = 0;
+ abi_suffix_len = 0;
+ target_resolved = false;
+ abi_pass = NAMEI_ABI_PASS_ABI;
#ifdef INVARIANTS
KASSERT((ndp->ni_debugflags & NAMEI_DBG_CALLED) == 0,
("%s: repeated call to namei without NDREINIT", __func__));
@@ -655,7 +669,15 @@ restart:
ndp->ni_loopcnt = 0;
ndp->ni_vp = NULL;
- error = namei_getpath(ndp);
+ if (__predict_false(abi_pass == NAMEI_ABI_PASS_SYMLINK)) {
+ /*
+ * The expansion was installed when abi_pass became
+ * SYMLINK. It is not reloaded from userspace.
+ */
+ MPASS(cnp->cn_pnbuf != NULL);
+ error = 0;
+ } else
+ error = namei_getpath(ndp);
if (__predict_false(error != 0)) {
namei_cleanup_cnp(cnp);
nameicap_cleanup(ndp, error);
@@ -734,6 +756,8 @@ restart:
* Locked lookup.
*/
for (;;) {
+ size_t prev_pathlen;
+
ndp->ni_startdir = dp;
error = vfs_lookup(ndp);
if (error != 0) {
@@ -746,10 +770,56 @@ restart:
error == ENOENT && was_restarted == 0;
if (error != ERESTART && !abi_restart)
goto out;
+ /*
+ * One restart from the native root is allowed.
+ * abi_pass only moves ABI -> SYMLINK or ABI ->
+ * NATIVE; never backwards. If the target of an
+ * absolute symlink had not yet resolved when the
+ * ABI pass failed, restart walking the saved
+ * expansion in place of the original path: the
+ * target may name a file that exists only in the
+ * native namespace (host-integration symlinks
+ * under the ABI root, linprocfs' /proc/<pid>/exe).
+ * An ENOENT after the target resolved, or with
+ * nothing saved, restarts from the original path.
+ *
+ * The old path buffer is released before the
+ * expansion is installed, so namei_cleanup_cnp()
+ * does not free the saved symlink.
+ *
+ * vfs_lookup() subtracts a component from
+ * ni_pathlen before looking it up, so a miss on
+ * the target's own last component leaves exactly
+ * the suffix; only less than that means the walk
+ * had moved past the target.
+ */
+ if (abi_symlink != NULL &&
+ ndp->ni_pathlen < abi_suffix_len)
+ target_resolved = true;
nameicap_cleanup(ndp, error);
pwd_drop(pwd);
namei_cleanup_cnp(cnp);
NDRESET(ndp);
+ if (abi_restart && abi_symlink != NULL &&
+ !target_resolved) {
+ KASSERT(abi_pass == NAMEI_ABI_PASS_ABI,
+ ("%s: symlink restart from pass %d",
+ __func__, abi_pass));
+ abi_pass = NAMEI_ABI_PASS_SYMLINK;
+ cnp->cn_pnbuf = abi_symlink;
+ ndp->ni_pathlen = abi_symlink_len;
+ abi_symlink = NULL;
+ } else {
+ if (abi_restart) {
+ KASSERT(abi_pass ==
+ NAMEI_ABI_PASS_ABI,
+ ("%s: native restart from pass %d",
+ __func__, abi_pass));
+ abi_pass = NAMEI_ABI_PASS_NATIVE;
+ }
+ uma_zfree(namei_zone, abi_symlink);
+ abi_symlink = NULL;
+ }
if (abi_restart)
was_restarted = ISRESTARTED;
ndp->ni_cnd.cn_flags |= was_restarted;
@@ -764,14 +834,19 @@ restart:
ndp->ni_vp, false, ndp);
nameicap_cleanup(ndp, 0);
pwd_drop(pwd);
+ uma_zfree(namei_zone, abi_symlink);
NDVALIDATE(ndp);
return (0);
}
+ prev_pathlen = ndp->ni_pathlen;
error = namei_follow_link(ndp);
if (error != 0)
break;
vput(ndp->ni_vp);
dp = ndp->ni_dvp;
+ if (abi_symlink != NULL &&
+ prev_pathlen <= abi_suffix_len)
+ target_resolved = true;
/*
* Check if root directory should replace current directory.
*/
@@ -789,10 +864,38 @@ restart:
*/
if ((cnp->cn_flags & ISRESTARTED) != 0)
ndp->ni_rootdir = pwd->pwd_rdir;
+ else if (pwd->pwd_adir != pwd->pwd_rdir) {
+ /*
+ * Save the expansion, and the length of
+ * the not-yet-walked suffix appended past
+ * the target, so that should the target
+ * fail to resolve under the ABI root the
+ * ENOENT handling above can restart from
+ * the expansion in place of the original
+ * path.
+ */
+ if (abi_symlink == NULL)
+ abi_symlink = uma_zalloc(namei_zone,
+ M_WAITOK);
+ memcpy(abi_symlink, cnp->cn_pnbuf,
+ ndp->ni_pathlen);
+ abi_symlink_len = ndp->ni_pathlen;
+ abi_suffix_len = prev_pathlen;
+ target_resolved = false;
+ }
vrele(dp);
error = namei_handle_root(ndp, &dp);
if (error != 0)
goto out;
+ } else if (target_resolved) {
+ /*
+ * Target already consumed under the ABI root
+ * (relative symlink at or past the end of the
+ * saved expansion). A later ENOENT is no
+ * grounds to restart from that expansion.
+ */
+ uma_zfree(namei_zone, abi_symlink);
+ abi_symlink = NULL;
}
}
vput(ndp->ni_vp);
@@ -804,6 +907,7 @@ out:
namei_cleanup_cnp(cnp);
nameicap_cleanup(ndp, error);
pwd_drop(pwd);
+ uma_zfree(namei_zone, abi_symlink);
return (error);
}