git: 23b326728778 - main - namei: retry absolute symlink expansions from the native root

From: Devin Teske <dteske_at_FreeBSD.org>
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);
 }