Page MenuHomeFreeBSD

namei: retry absolute symlink expansions from the native root
Needs ReviewPublic

Authored by dteske on Sat, Aug 15, 6:22 AM.
Tags
None
Referenced Files
F167633564: D58855.id.diff
Sun, Aug 23, 8:53 AM
Unknown Object (File)
Sat, Aug 22, 3:52 AM
Unknown Object (File)
Fri, Aug 21, 10:59 PM
Unknown Object (File)
Fri, Aug 21, 12:49 PM
Unknown Object (File)
Thu, Aug 20, 3:36 PM
Unknown Object (File)
Wed, Aug 19, 3:24 PM
Unknown Object (File)
Mon, Aug 17, 7:41 PM
Unknown Object (File)
Mon, Aug 17, 12:34 PM

Details

Summary

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.

Give absolute symlink targets the same overlay treatment original
paths receive: remember the expansion produced while following an
absolute symlink during the ABI pass and, when that pass fails with
ENOENT, retry the saved expansion from the native root before
falling back to the original path. 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.

Add tests exercising the decision table: a target under the ABI root
wins over an identically named native file (PR 289739), native-only
targets fall back (PR 297426) including through a mid-path directory
symlink (the fontconfig shape), and the plain no-symlink native
fallback is unchanged.

PR: 297426
Fixes: 459ac3044b51 ("namei: Preserve ABI root for absolute symlinks before fallback")
MFC after: 1 month

Test Plan

Four test categories:

  1. linux-proc-self-exe-litmus2 (from PR 297426)
  2. /compat/linux/bin/fc-match sans
  3. security/linux-rl9-ca-certificates port (arrowd)
  4. manual symlink resolution vs follow (Alex S)

Category 1: linux-proc-self-exe-litmus2

Setup for testing ...

a. sudo pkg install linux-rl9-devtools
b. sudo service linux onestart
c. fetch -o linux-proc-self-exe-litmus2.c https://bugs.freebsd.org/bugzilla/attachment.cgi?id=273783
d. /compat/linux/usr/bin/gcc -O2 -o linux-proc-self-exe-litmus2 linux-proc-self-exe-litmus2.c
e. sudo mkdir -p /compat/linux/tmp
f. sudo chmod 1777 /compat/linux/tmp (purposeful -- easy to whack at end)
g. cp linux-proc-self-exe-litmus2 /compat/linux/tmp

Perform testing ...

a. ./linux-proc-self-exe-litmus2
b. /compat/linux/tmp/linux-proc-self-exe-litmus2
c. sudo chroot /compat/linux /tmp/linux-proc-self-exe-litmus2

NOTE: Expect VERDICT: PASS on all tests under patched system. Test warns if you have *native* proc mounted on /proc to prevent false-positives on unpatched systems with procfs mounted (tests should be performed with linprocfs only)

Category 2: /compat/linux/bin/fc-match

Perform testing ...

a. dwatch -q -F -N 2 -k fc-match -t 'this->namei_error != ENOENT && strstr(this->namei_pathstr, "/etc/fonts")!=NULL' -X namei -- -c "env -u FONTCONFIG_PATH /compat/linux/usr/bin/fc-match sans"

b. dwatch -q -F -N 2 -k fc-match -t 'this->namei_error != ENOENT && strstr(this->namei_pathstr, "/etc/fonts")!=NULL' -X namei -- -c "env FONTCONFIG_PATH=/etc/fonts /compat/linux/usr/bin/fc-match sans"

c. dwatch -q -F -N 2 -k fc-match -t 'this->namei_error != ENOENT && strstr(this->namei_pathstr, "/etc/fonts")!=NULL' -X namei -- -c "env FONTCONFIG_PATH=/usr/local/etc/fonts /compat/linux/usr/bin/fc-match sans"

d. dwatch -q -F -N 2 -k fc-match -t 'this->namei_error != ENOENT && strstr(this->namei_pathstr, "/etc/fonts")!=NULL' -X namei -- -c "env FONTCONFIG_PATH=/compat/linux/etc/fonts /compat/linux/usr/bin/fc-match sans"

NOTE: On an unpatched system, you'll get a font other than DejaVuSans.ttf: "DejaVu Sans" "Book" and you may be lacking the vfs:namei:lookup:return traversal

Category 3: security/linux-rl9-ca-certificates port (arrowd)

Setup ...

a. sudo pkg install poudriere
b. sudo service linux onestart
c. sudo sysrc -f /usr/local/etc/poudriere.conf ZPOOL=zroot ZROOTFS=/poudriere (host specific)
d. sudo sysrc -f /usr/local/etc/poudriere.conf NOLINUX=1 (needed or poudriere testport -i will fail)
e. sudo poudriere jail -c -j 160 -v 16 -m pkgbase=base_latest -U pkg+https://pkg.freebsd.org/ (1m43s)
f. sudo poudriere ports -c -p default -m null -M /usr/ports

Perform testing ...

a. sudo poudriere testport -j 160 -p default -i security/linux-rl9-ca-certificates
b. make test
c. chmod u+w "$(make -V STAGEDIR)$(make -V PREFIX)/etc/pki/ca-trust/extracted/pem/directory-hash"
(chmod needed because directory-hash is 555, and will make below clean fail)
d. make clean stage
e. make check-plist test
f. make deinstall
g. make test

NOTE: Same sequence as the comment above do-test in the port Makefile with an extra chmod before make clean because directory-hash is left mode 555. Expect ok on every make test, ensuring patch did not introduce any regression.

Category 4: manual symlink resolution vs follow (Alex S)

Setup ...

a. fetch -o links.rb https://gist.githubusercontent.com/shkhln/8c252c2ca2a4e7e6b012c8944ea67298/raw
b. chmod +x links.rb
c. sudo pkg install ruby (if needed)

Perform testing ...

a. sudo ./links.rb (calls mount ... tmpfs ...)
b. sudo ./links.rb (calls umount ...)

NOTE: First run mounts then prints either OK or got "fail", expected ... per link; second run tears down. Expect OK for every line with "absolute:" (relative/beyond are out of scope of namei at this time)

Diff Detail

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

Event Timeline

dteske added a reviewer: emulation.
dteske edited the test plan for this revision. (Show Details)
dteske edited the test plan for this revision. (Show Details)
dteske edited the test plan for this revision. (Show Details)

Could you please test security/linux-rl9-ca-certificates against this change? See the comment near do-test in the Makefile.

I also have a test you might find useful: https://gist.github.com/shkhln/8c252c2ca2a4e7e6b012c8944ea67298. This one is supposed to verify whether the logic agrees with "manual" symlink resolution where a user (or an app) reads the path from the symlink before passing it somewhere, which I think is a desirable property here.

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

Could you please test security/linux-rl9-ca-certificates against this change? See the comment near do-test in the Makefile.

Tested. Both on unpatched GENERIC kernel and patched kernel.

Unpatched kernel (inside poudriere jail running 16.0 latest):

nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ make test
===>  Testing for linux-rl9-ca-certificates-2025.2.80
/bin/sh -c '/compat/linux/bin/env XDG_CONFIG_HOME="/wrkdirs/usr/ports/security/linux-rl9-ca-certificates/work/.config"  /bin/trust list' | wc -l | ((read count ; [ $count != 0 ]) && echo ok)
ok
nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ chmod u+w "$(make -V STAGEDIR)$(make -V PREFIX)/etc/pki/ca-trust/extracted/pem/directory-hash"
nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ make clean stage
...
nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ make check-plist test
====> Checking for pkg-plist issues (check-plist)
===> Parsing plist
===> Checking for items in STAGEDIR missing from pkg-plist
===> Checking for items in pkg-plist which are not in STAGEDIR
===> No pkg-plist issues found (check-plist)
===>  Testing for linux-rl9-ca-certificates-2025.2.80
/bin/sh -c '/compat/linux/bin/env XDG_CONFIG_HOME="/wrkdirs/usr/ports/security/linux-rl9-ca-certificates/work/.config"  /bin/trust list' | wc -l | ((read count ; [ $count != 0 ]) && echo ok)
ok
nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ make deinstall
...
nobody@160-default:/usr/ports/security/linux-rl9-ca-certificates $ make test
===>  Testing for linux-rl9-ca-certificates-2025.2.80
/bin/sh -c '/compat/linux/bin/env XDG_CONFIG_HOME="/wrkdirs/usr/ports/security/linux-rl9-ca-certificates/work/.config"  /bin/trust list' | wc -l | ((read count ; [ $count != 0 ]) && echo ok)
ok

ok results across the board.

Re-ran under patched kernel.

Also ok results across the board. No regression.

I also have a test you might find useful: https://gist.github.com/shkhln/8c252c2ca2a4e7e6b012c8944ea67298. This one is supposed to verify whether the logic agrees with "manual" symlink resolution where a user (or an app) reads the path from the symlink before passing it somewhere, which I think is a desirable property here.

Here's the results:

dteske@FreeBSD ~ $ diff -U90 links-rb-unpatched.txt links-rb-patched.txt 
--- links-rb-unpatched.txt        2026-08-15 09:05:37.004070000 -0700
+++ links-rb-patched.txt        2026-08-15 08:54:54.814917000 -0700
@@ -1,14 +1,14 @@
 /links/foo-absolute: OK
 /links/foo-relative: OK
 /links/foo-beyond-abi-root: OK
-/links/bar-absolute: got "fail", expected "bar from base"
+/links/bar-absolute: OK
 /links/bar-relative: got "fail", expected "bar from base"
 /links/bar-beyond-abi-root: got "fail", expected "bar from base"
 /links/baz-absolute: OK
 /links/baz-relative: OK
 /links/baz-beyond-abi-root: OK
-/links/broken-absolute: got "fail", expected ""
+/links/broken-absolute: OK
 /links/broken-relative: got "fail", expected ""
 then
 mount: _: Device busy
 Unmounting /home/dteske/test-dir

@emaste Can I get your eyes on this? I think you above all others understand what is at stake here.

All linux ports that install outside emul_path are presently broken in 16.0-CURRENT. I want to make sure we are making headway to correct that.

I've tested this patch successfully.

With this patch, absolute symlinks work again for Linux ports, such as linux-rl9-fontconfig, linux-brave, etc. and to our other reviewers points, does not seem to break anything, only help.

Worth noting that the issues with /proc/self/exe or /proc/<pid>/exe is related to this breakage, that you helpfully already identified the root cause on (459ac3044b51), and with this fix we don't need any complicated linprocfs patch addressing just those symlinks, as this is the global fix.