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
F171781734: D58855.diff
Sun, Sep 13, 12:04 PM
Unknown Object (File)
Sat, Sep 12, 9:38 AM
Unknown Object (File)
Sat, Sep 12, 4:11 AM
Unknown Object (File)
Sat, Sep 12, 12:23 AM
Unknown Object (File)
Thu, Sep 10, 11:19 PM
Unknown Object (File)
Thu, Sep 10, 4:28 AM
Unknown Object (File)
Wed, Sep 9, 10:49 PM
Unknown Object (File)
Wed, Sep 9, 5:43 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.

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.

Add tests exercising the decision table: a target under the ABI root
wins over an identically named native file (PR 289739), native-only
targets restart from the expansion (PR 297426) including through a
mid-path directory symlink (as with linux-rl9-fontconfig), an ENOENT
past a target that resolved under the ABI root is not retried from
the expansion even when the missing suffix exists natively, 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.

How this behaves when there is more than one symlink in the path to resolve?

In D58855#1359474, @kib wrote:

How this behaves when there is more than one symlink in the path to resolve?

Correctly. Same as one abs symlink.

Only the latest absolute target is kept for a possible native fallback; on ENOENT that target is retried once from the native root, then any further symlinks follow normal rules. Relative symlinks resolve as usual — they just do not affect that fallback.

In D58855#1359474, @kib wrote:

How this behaves when there is more than one symlink in the path to resolve?

Correctly. Same as one abs symlink.

Only the latest absolute target is kept for a possible native fallback; on ENOENT that target is retried once from the native root, then any further symlinks follow normal rules. Relative symlinks resolve as usual — they just do not affect that fallback.

How the 'correctly' is defined? It is very strange to have some symlink in the whole resolution path to be handled specially.

Before the change, the whole lookuo either happen under the native root, or if failed, falls back to the ABI-specific root. This is at least logical. Why the last symlink should be special in the generic VFS layer?
I understand that this might give you the behavior tailored for your specific configuration. But offering it as a mechanism for namei(9) is IMO not correct.

In D58855#1359749, @kib wrote:
In D58855#1359474, @kib wrote:

How this behaves when there is more than one symlink in the path to resolve?

Correctly. Same as one abs symlink.

Only the latest absolute target is kept for a possible native fallback; on ENOENT that target is retried once from the native root, then any further symlinks follow normal rules. Relative symlinks resolve as usual — they just do not affect that fallback.

How the 'correctly' is defined?

Abs target gets same overlay as original path: ABI first, native on ENOENT.

It is very strange to have some symlink in the whole resolution path to be handled specially. [...] Why the last symlink should be special in the generic VFS layer?

Abs follow replaces path; only one current target remains; target may get the native retry. Same rule for one abs link or many, not as "last symlink" rule.

Before the change, the whole lookuo either happen under the native root, or if failed, falls back to the ABI-specific root. This is at least logical.

You've got the order inverted. Lookup is ABI root first, native on ENOENT.

... this might give you the behavior tailored for your specific configuration.

The failures are wider than one config: abs links under the emul root to native-only targets:

  1. fontconfig
  2. alsa-lib
  3. ca-certificates
  4. linprocfs /proc/<pid>/exe and /proc/self/exe

(a linprocfs patch was initially tried, but it does not cover everything, such as /etc/fonts -> /usr/local/etc/fonts)

But offering it as a mechanism for namei(9) is IMO not correct.

Abs-target policy in namei is already precedent you signed-off on:

  1. D40479 / cea7c564 -- you accepted
  2. 459ac3044b516af698d7342431bdbcf784fdf7a4 / https://github.com/freebsd/freebsd-src/pull/2166 -- Reviewed by: kib (PR 289739)

459ac304 keeps abs expansion on the ABI pass; restarting the original path cannot recover a native-only target. This gives that expansion the same two-passes the original path already has. Succeed-under-ABI still wins.

If abs-target policy in namei was right for D40479 and 459ac304, what makes finishing the same overlay for native-only targets the wrong layer? If I have the two-pass model wrong, I want to hear it.

Absent a namei fix, post-459ac304 those stay broken (fontconfig, alsa, ca-certs, /proc/self/exe). Is leaving the system broken acceptable? Reverting 459ac304 only takes us back to PR 289739.

Will test this on a VM as I never install Linux ports as they're outdated and most likely full of security issues. Left a suggestion.

sys/kern/vfs_lookup.c
786–791

We're checking for abi_symlink != NULL twice.

Let me formulate what I tried to say, differently. There should be no abi symlink pass, or, if you prefer such wording, there must be at least one swich from the ABI namespace to native namespace. If we get ENOENT and the failed component was provided by an absolute symlink, then we should restart native-rooted lookup from the symlink resolved and the rest of the unprocessed path. If we successfully resolved symlink target in the ABI root and ENOENT came later, it is not the reason to restart with the symlink in native root.

I think that your patch does almost that, because there it could be only one restart, symlink_retry is not happening if ISRESTARTED was already set. But it is not quite as is, since it remembers the last symlink' READLINK() result and always starts with it. This part is not correct IMO, the 'start native pass with the symlink path' should only happen if the symlink target was not resolved in the ABI root. If we found the symlink target and ENOENT is caused by next steps in the path walk, it should not be used.

Also the comment etry the expansion from the native root before falling back to the original path seems to be not correct because there can be only one restart.

sys/kern/vfs_lookup.c
786–791

Good catch. Will fold into next diff (this hunk is being reworked
for the stricter retry condition anyway).

In D58855#1360244, @kib wrote:

Let me formulate what I tried to say, differently. There should be no abi symlink pass, or, if you prefer such wording, there must be at least one swich from the ABI namespace to native namespace. If we get ENOENT and the failed component was provided by an absolute symlink, then we should restart native-rooted lookup from the symlink resolved and the rest of the unprocessed path. If we successfully resolved symlink target in the ABI root and ENOENT came later, it is not the reason to restart with the symlink in native root.

Agreed. That is how it should work.

I think that your patch does almost that, because there it could be only one restart, symlink_retry is not happening if ISRESTARTED was already set. But it is not quite as is, since it remembers the last symlink' READLINK() result and always starts with it. This part is not correct IMO, the 'start native pass with the symlink path' should only happen if the symlink target was not resolved in the ABI root. If we found the symlink target and ENOENT is caused by next steps in the path walk, it should not be used.

Agreed. Will restrict the retry to when the target itself did not resolve under the ABI root; ENOENT after the target resolved stays ENOENT. The driving cases (fontconfig, alsa-lib, ca-certificates, /proc/<pid>/exe) all fail on the first component of the expansion, so the stricter condition covers them all.

Also the comment etry the expansion from the native root before falling back to the original path seems to be not correct because there can be only one restart.

Correct -- only one restart -- will fix the comment.

New diff shortly.

Address review:

  • Retry only fires if the abs target had not yet resolved when the ABI pass failed (ni_pathlen vs abi_suffix_len); ENOENT past a resolved target restarts from the original path as before.
  • Rewrite comment to state the rule and the one-restart limit.
  • Fold duplicate abi_symlink != NULL checks into one block (rbranco).
  • New test: symlink_target_resolved_suffix pins the resolved-target case.

Retested: full test plan passing on the patched kernel.

I tried to understand the patch' logic, but I cannot.

In particular, I completely miss how the check prev_pathlen <= abi_suffix_len means anything, which is done when finding the relative symlink. We should care about absolute symlinks only, no?
Also, the restart control is now smeared among 5 or 6 variables, depending on how to count it.

I think we can do much better. I propose, only for the discussion right now, to remove all the restart logic from the namei(9) function. Lets rename namei(9) to namei_nat(9) after the removal, and put the following into namei(9):

  • (1) if the process does not have abi root (pwd_adir == pwd_rdir) just do return (namei_nat(ndp));. I believe this change alone would make some people happy.
  • (2) if the process has the the root, dup the ndp structure, paying attention to ref the vnodes etc, set root to pwd_adir and call namei_nat(9)
  • (3) if the previous call returned ENOENT, set root to pwd_rdir, and call namei_nat(9)

After this refactoring, we could modify namei_nat() in the style of this patch: storing the result of reading of the last absolute symlink in ndp->nd_abi_symlink (new field) and indicating that the ENOENT was during the walk of the symlink components.
There, if (3) found nd_abi_symlink and the indicator, we add the step 3a, calling namei_nat(9) for the abi_symlink + rest of the cn_nameptr.

IMO not trying to mix the namei() loop and restart would make the logic much less convoluted, and provides the useful optimization for the native case.

sys/kern/vfs_lookup.c
803

There is no need to check for abi_symlink != NULL, uma_zfree() handles NULL on its own.

I think, that if what I propose could be made working, it can be improved further. Instead of making namei() check for pwd_adir, we can provide ABI-specific namei method, say sv_namei in struct sysent. The native FreeBSD ABI would use current namei(9) stripped from the restart. The Linux ABI would provide the 'restartable' namei() which uses native namei() for each restart.

In D58855#1366432, @kib wrote:

In particular, I completely miss how the check prev_pathlen <= abi_suffix_len means anything, which is done when finding the relative symlink. We should care about absolute symlinks only, no?

Correct, we only care about absolute symlinks. The prev_pathlen comparison tells us when it is safe to perform housekeeping (by dropping a previously stashed pathname buffer; cnp->cn_pnbuf copied into abi_symlink) in the event that we are finally finished walking the absolute symlink elements under the emul root wherein previous ni_pathlen (stored in prev_pathlen) shrank to the leftover suffix as we processed each path element.

I can concede that this is a bit difficult to process and offer as a solution the introduction of a bool to more cleanly illustrate the state we are tracking. Something like "bool target_resolved" set to false when we make a copy of the pathname buffer stashed in abi_symlink and then later flipped to true when the walk has finished the absolute symlink's destination under the emul root. That should make it easier to read the relative else-if and the ENOENT restart by testing that flag instead of prev_pathlen <= abi_suffix_len / ni_pathlen >= abi_suffix_len

Also, the restart control is now smeared among 5 or 6 variables, depending on how to count it.

I can eliminate the len vars (prev_pathlen and abi_suffix_len) in favor of the proposed target_resolved bool so relative else-if and ENOENT restart. That would reduce the "smear" from 5 or 6 variables down to 4 (abi_symlink, abi_symlink_len, target_resolved, abi_symlink_pass).

I think we can do much better. I propose, only for the discussion right now, to remove all the restart logic from the namei(9) function. Lets rename namei(9) to namei_nat(9) after the removal, and put the following into namei(9):

  • (1) if the process does not have abi root (pwd_adir == pwd_rdir) just do return (namei_nat(ndp));. I believe this change alone would make some people happy.
  • (2) if the process has the the root, dup the ndp structure, paying attention to ref the vnodes etc, set root to pwd_adir and call namei_nat(9)
  • (3) if the previous call returned ENOENT, set root to pwd_rdir, and call namei_nat(9)

Discussing:

(1) alone is already a win.

(2) and (3) match what we do today and would risk losing the fix for /proc/self/exe because we still need to handle the case where ENOENT occurs inside the absolute symlink expansion (your proposed 3a below).

After this refactoring, we could modify namei_nat() in the style of this patch: storing the result of reading of the last absolute symlink in ndp->nd_abi_symlink (new field) and indicating that the ENOENT was during the walk of the symlink components.
There, if (3) found nd_abi_symlink and the indicator, we add the step 3a, calling namei_nat(9) for the abi_symlink + rest of the cn_nameptr.

IMO not trying to mix the namei() loop and restart would make the logic much less convoluted, and provides the useful optimization for the native case.

In D58855#1366432, @kib wrote:

In particular, I completely miss how the check prev_pathlen <= abi_suffix_len means anything, which is done when finding the relative symlink. We should care about absolute symlinks only, no?

Correct, we only care about absolute symlinks. The prev_pathlen comparison tells us when it is safe to perform housekeeping (by dropping a previously stashed pathname buffer; cnp->cn_pnbuf copied into abi_symlink) in the event that we are finally finished walking the absolute symlink elements under the emul root wherein previous ni_pathlen (stored in prev_pathlen) shrank to the leftover suffix as we processed each path element.

Yes, but the check is done when the symlink is not absolute.

I can concede that this is a bit difficult to process and offer as a solution the introduction of a bool to more cleanly illustrate the state we are tracking. Something like "bool target_resolved" set to false when we make a copy of the pathname buffer stashed in abi_symlink and then later flipped to true when the walk has finished the absolute symlink's destination under the emul root. That should make it easier to read the relative else-if and the ENOENT restart by testing that flag instead of prev_pathlen <= abi_suffix_len / ni_pathlen >= abi_suffix_len

Also, the restart control is now smeared among 5 or 6 variables, depending on how to count it.

I can eliminate the len vars (prev_pathlen and abi_suffix_len) in favor of the proposed target_resolved bool so relative else-if and ENOENT restart. That would reduce the "smear" from 5 or 6 variables down to 4 (abi_symlink, abi_symlink_len, target_resolved, abi_symlink_pass).

If continuing with this patch, I suggest to add a single control state variable (enum) which would indicate which kind of restart we currently processing, like {ABI root}=>{symlink restart}=>{native root} etc. Then all other variables would only carry the aux data needed to proceed, and also we can e.g. assert that we never go back in the state, like switch from {symlink restart} to {ABI root}.

Perhaps commit the test cases already, regardless of the main patch. Just disable it for CI jobs.

I think we can do much better. I propose, only for the discussion right now, to remove all the restart logic from the namei(9) function. Lets rename namei(9) to namei_nat(9) after the removal, and put the following into namei(9):

  • (1) if the process does not have abi root (pwd_adir == pwd_rdir) just do return (namei_nat(ndp));. I believe this change alone would make some people happy.
  • (2) if the process has the the root, dup the ndp structure, paying attention to ref the vnodes etc, set root to pwd_adir and call namei_nat(9)
  • (3) if the previous call returned ENOENT, set root to pwd_rdir, and call namei_nat(9)

Discussing:

(1) alone is already a win.

(2) and (3) match what we do today and would risk losing the fix for /proc/self/exe because we still need to handle the case where ENOENT occurs inside the absolute symlink expansion (your proposed 3a below).

After this refactoring, we could modify namei_nat() in the style of this patch: storing the result of reading of the last absolute symlink in ndp->nd_abi_symlink (new field) and indicating that the ENOENT was during the walk of the symlink components.
There, if (3) found nd_abi_symlink and the indicator, we add the step 3a, calling namei_nat(9) for the abi_symlink + rest of the cn_nameptr.

IMO not trying to mix the namei() loop and restart would make the logic much less convoluted, and provides the useful optimization for the native case.

Exactly.

In D58855#1367664, @kib wrote:
In D58855#1366432, @kib wrote:

In particular, I completely miss how the check prev_pathlen <= abi_suffix_len means anything, which is done when finding the relative symlink. We should care about absolute symlinks only, no?

Correct, we only care about absolute symlinks. The prev_pathlen comparison tells us when it is safe to perform housekeeping (by dropping a previously stashed pathname buffer; cnp->cn_pnbuf copied into abi_symlink) in the event that we are finally finished walking the absolute symlink elements under the emul root wherein previous ni_pathlen (stored in prev_pathlen) shrank to the leftover suffix as we processed each path element.

Yes, but the check is done when the symlink is not absolute.

Yes, and it has to be.

Imagine the following case: path /a/b/c

Where b is an absolute symlink and c is a relative symlink.

/a dir -> contains b symlink (absolute) -> points to /t dir -> contains c symlink (relative) -> points to xxx/d file, where xxx does not exist under the ABI root.

  1. at b: prev_pathlen = 3 (the /c that's left, plus the NUL)
  2. exzpansion then becomes /t/c so abi_symlink_len = 5, abi_suffix_len = 3
  3. /t resolves, and by the time we reach c there's nothing left but the NUL: prev_pathlen = 1 <= 3, so the target resolved and we drop the stashed abi_symlink
  4. without that drop: c expands to xxx/d file, ni_pathlen = 6, then xxx fails with ni_pathlen (3) >= abi_suffix_len (3), and we restart from /t/c under the native root because of a failure inside an unrelated [relative] target

and the else block, it's not just "symlink is relative, discard" but rather we skip it when a relative symlink was found while prev_ptahlen was still above the count that puts us as still inside a target that has not yet been resolved (in which case we don't want to drop the stashed abi_symlink).

But we're going to rewrite it to make it better; just taking a moment to explain it -- not trying to defend it. I like your suggested approach(es) better. However, you suggested two different approaches, need to know which one you want. Do you want the larger refactor or just a minimal change that focuses on adding enums?

I can concede that this is a bit difficult to process and offer as a solution the introduction of a bool to more cleanly illustrate the state we are tracking. Something like "bool target_resolved" set to false when we make a copy of the pathname buffer stashed in abi_symlink and then later flipped to true when the walk has finished the absolute symlink's destination under the emul root. That should make it easier to read the relative else-if and the ENOENT restart by testing that flag instead of prev_pathlen <= abi_suffix_len / ni_pathlen >= abi_suffix_len

Also, the restart control is now smeared among 5 or 6 variables, depending on how to count it.

I can eliminate the len vars (prev_pathlen and abi_suffix_len) in favor of the proposed target_resolved bool so relative else-if and ENOENT restart. That would reduce the "smear" from 5 or 6 variables down to 4 (abi_symlink, abi_symlink_len, target_resolved, abi_symlink_pass).

If continuing with this patch, I suggest to add a single control state variable (enum) which would indicate which kind of restart we currently processing, like {ABI root}=>{symlink restart}=>{native root} etc. Then all other variables would only carry the aux data needed to proceed, and also we can e.g. assert that we never go back in the state, like switch from {symlink restart} to {ABI root}.

That sounds very elegant.

Perhaps commit the test cases already, regardless of the main patch. Just disable it for CI jobs.

I'm definitely behind that decision 100% and I'll spin up a new review that adds the tests (tests/sys/vfs/abi_root_symlink.sh) with the change to tests/sys/vfs/Makefile

As for disabling in CI, correct me if wrong but CI doesn't have a Linux base to run these tests and they will automatically be skipped.

For opt-in testing (where people have a proper Linux base setup), I'll flip the two affected cases (symlink_target_native_only and symlink_target_native_only_midpath) to atf_expect_fail so they report expected_failure until the fix is in.

How does that sound?