git: b98d169d1f91 - main - tests/sys/vfs: add ABI-root absolute symlink tests

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Sat, 26 Sep 2026 18:16:28 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=b98d169d1f910e0b28c7ea2f26fcc09f83614ec9

commit b98d169d1f910e0b28c7ea2f26fcc09f83614ec9
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-09-26 18:12:33 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-09-26 18:12:33 +0000

    tests/sys/vfs: add ABI-root absolute symlink tests
    
    Pin the overlay namei(9) uses for Linux ABI processes: a target under
    the ABI root wins (PR 289739), a native-only target should retry from
    the native root (PR 297426), an ENOENT past a resolved ABI target is
    not retried, and the plain no-symlink native fallback is unchanged.
    Skip without a Linux userland so CI stays green.  The two native-only
    cases use atf_expect_fail until the retry lands.
    
    PR:             297426
    Reviewed by:    kib
    Differential Revision:  https://reviews.freebsd.org/D59990
---
 tests/sys/vfs/Makefile            |   2 +
 tests/sys/vfs/abi_root_symlink.sh | 279 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 281 insertions(+)

diff --git a/tests/sys/vfs/Makefile b/tests/sys/vfs/Makefile
index e25282b2c70a..f34616e22ebf 100644
--- a/tests/sys/vfs/Makefile
+++ b/tests/sys/vfs/Makefile
@@ -5,6 +5,8 @@ TESTSDIR=		${TESTSBASE}/sys/vfs
 ATF_TESTS_C+=	lookup_cap_dotdot
 CFLAGS.lookup_cap_dotdot.c+=	-I${SRCTOP}/tests
 
+ATF_TESTS_SH+=	abi_root_symlink
+
 #ATF_TESTS_SH+=	lookup_test
 
 TAP_TESTS_SH+=	trailing_slash
diff --git a/tests/sys/vfs/abi_root_symlink.sh b/tests/sys/vfs/abi_root_symlink.sh
new file mode 100644
index 000000000000..18f7b5915a28
--- /dev/null
+++ b/tests/sys/vfs/abi_root_symlink.sh
@@ -0,0 +1,279 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+#
+# Regression tests for absolute symlink resolution under an ABI root
+# (compat.linux.emul_path).  namei(9) resolves paths for Linux ABI
+# processes by trying the ABI root first and falling back to the
+# native root.  Absolute symlink targets receive the same treatment:
+# a target is first resolved under the ABI root (so self-contained
+# Linux userlands keep working, PR 289739); when the target itself
+# does not resolve there, the walk restarts once from the native
+# root using the expansion (so symlinks pointing at host paths, and
+# linprocfs' /proc/<pid>/exe, keep working, PR 297426).  An ENOENT
+# past a target that did resolve under the ABI root falls back to
+# the original path like any other lookup.
+#
+# These tests require the Linux ABI (linux_enable=YES) and a Linux
+# userland (e.g. emulators/linux_base-rl9) providing ${emul_path}/bin/sh.
+# Without that, they skip, so CI jobs that lack a Linux base do not
+# fail.  The two native-only cases still fail on CURRENT namei(9) and
+# use atf_expect_fail until the native-root retry lands (PR 297426).
+# Each case comments the fixture so it can be rebuilt by hand.
+# $wd is any empty directory (the ATF work directory when kyua
+# runs).  $emul is compat.linux.emul_path, usually /compat/linux.
+# $emul$wd means the same path names created under that ABI root.
+#
+
+require_linux()
+{
+	emul=$(sysctl -n compat.linux.emul_path 2>/dev/null)
+	[ "$emul" ] || atf_skip "Linux ABI not present (compat.linux.emul_path)"
+	kldstat -q -m linux64 || kldstat -q -m linux ||
+		atf_skip "linux(4) not loaded"
+	lsh="$emul/bin/sh"
+	[ -x "$lsh" ] || atf_skip "Linux userland not installed ($lsh)"
+	# Mirror of the ATF work directory under the ABI root.
+	wd=$(pwd)
+	abiwd="$emul$wd"
+	atf_check mkdir -p "$abiwd"
+}
+
+cleanup_linux()
+{
+	emul=$(sysctl -n compat.linux.emul_path 2>/dev/null)
+	[ "$emul" ] || return 0
+	rm -rf "$emul$(pwd)" 2>/dev/null
+	# Remove now-empty parents mirrored under the ABI root.
+	rmdir -p "$emul$(dirname "$(pwd)")" 2>/dev/null
+	return 0
+}
+
+atf_test_case symlink_target_in_abi_root cleanup
+symlink_target_in_abi_root_head()
+{
+	atf_set "descr" "Absolute symlink target under the ABI root wins" \
+	    "over an identically named native file (PR 289739)"
+	atf_set "require.user" "root"
+}
+symlink_target_in_abi_root_body()
+{
+	#
+	# Recreate by hand.  $wd is any empty directory.  $emul is
+	# compat.linux.emul_path (usually /compat/linux).
+	#
+	# Native:
+	#     $wd/f-both
+	#         Regular file containing the word "native".
+	#
+	# ABI (the same path names created under $emul):
+	#     $emul$wd/f-both
+	#         Regular file containing the word "abi".
+	#     $emul$wd/link-both
+	#         Absolute symlink whose target is $wd/f-both.
+	#
+	# A Linux process opens $wd/link-both.  namei tries that path
+	# under the ABI root first ($emul$wd/link-both), follows the
+	# symlink to $wd/f-both, and stays on the ABI pass, so the
+	# file that is read is $emul$wd/f-both.
+	#
+	# Expected result: the read returns "abi".
+	#
+	require_linux
+	printf native > "$wd/f-both"
+	printf abi > "$abiwd/f-both"
+	atf_check ln -s "$wd/f-both" "$abiwd/link-both"
+	atf_check -o inline:"abi" "$lsh" -c "cat $wd/link-both"
+}
+symlink_target_in_abi_root_cleanup()
+{
+	cleanup_linux
+}
+
+atf_test_case symlink_target_native_only cleanup
+symlink_target_native_only_head()
+{
+	atf_set "descr" "Absolute symlink under the ABI root pointing at" \
+	    "a native-only file falls back to the native root (PR 297426)"
+	atf_set "require.user" "root"
+}
+symlink_target_native_only_body()
+{
+	#
+	# Recreate by hand.  $wd is any empty directory.  $emul is
+	# compat.linux.emul_path (usually /compat/linux).
+	#
+	# Native:
+	#     $wd/f-native
+	#         Regular file containing the word "native".
+	#
+	# ABI (the same path names created under $emul):
+	#     $emul$wd/link-native
+	#         Absolute symlink whose target is $wd/f-native.
+	#     There is no $emul$wd/f-native.
+	#
+	# A Linux process opens $wd/link-native.  namei tries that
+	# path under the ABI root first ($emul$wd/link-native) and
+	# follows the symlink to $wd/f-native.  That target does not
+	# exist under the ABI root, so the walk should restart once
+	# from the native root using the expansion and read
+	# $wd/f-native.
+	#
+	# Expected result: the read returns "native".
+	# On CURRENT namei this is ENOENT (atf_expect_fail; PR 297426).
+	#
+	require_linux
+	printf native > "$wd/f-native"
+	atf_check ln -s "$wd/f-native" "$abiwd/link-native"
+	atf_expect_fail \
+	    "PR 297426: abs symlink to native-only target fails under ABI root"
+	atf_check -o inline:"native" "$lsh" -c "cat $wd/link-native"
+}
+symlink_target_native_only_cleanup()
+{
+	cleanup_linux
+}
+
+atf_test_case symlink_target_native_only_midpath cleanup
+symlink_target_native_only_midpath_head()
+{
+	atf_set "descr" "Absolute directory symlink under the ABI root" \
+	    "pointing at a native-only directory falls back to the native" \
+	    "root for lookups through it (PR 297426), as with" \
+	    "linux-rl9-fontconfig's /etc/fonts -> /usr/local/etc/fonts"
+	atf_set "require.user" "root"
+}
+symlink_target_native_only_midpath_body()
+{
+	#
+	# Recreate by hand.  $wd is any empty directory.  $emul is
+	# compat.linux.emul_path (usually /compat/linux).
+	#
+	# Native:
+	#     $wd/ndir/
+	#         Directory.
+	#     $wd/ndir/f
+	#         Regular file containing the word "native".
+	#
+	# ABI (the same path names created under $emul):
+	#     $emul$wd/dirlink
+	#         Absolute symlink whose target is $wd/ndir.
+	#     There is no $emul$wd/ndir.
+	#
+	# A Linux process opens $wd/dirlink/f.  namei tries that path
+	# under the ABI root first ($emul$wd/dirlink/f), follows
+	# dirlink to $wd/ndir, and then wants $wd/ndir/f.  The
+	# directory does not exist under the ABI root, so the walk
+	# should restart once from the native root using the
+	# expansion and read $wd/ndir/f.  This is the
+	# linux-rl9-fontconfig case (/etc/fonts ->
+	# /usr/local/etc/fonts).
+	#
+	# Expected result: the read returns "native".
+	# On CURRENT namei this is ENOENT (atf_expect_fail; PR 297426).
+	#
+	require_linux
+	atf_check mkdir "$wd/ndir"
+	printf native > "$wd/ndir/f"
+	atf_check ln -s "$wd/ndir" "$abiwd/dirlink"
+	atf_expect_fail \
+	    "PR 297426: abs dir symlink to native-only target fails under ABI root"
+	atf_check -o inline:"native" "$lsh" -c "cat $wd/dirlink/f"
+}
+symlink_target_native_only_midpath_cleanup()
+{
+	cleanup_linux
+}
+
+atf_test_case symlink_target_resolved_suffix cleanup
+symlink_target_resolved_suffix_head()
+{
+	atf_set "descr" "ENOENT past an absolute symlink target that" \
+	    "resolved under the ABI root is not retried from the native" \
+	    "root, even when the missing suffix exists natively"
+	atf_set "require.user" "root"
+}
+symlink_target_resolved_suffix_body()
+{
+	#
+	# Recreate by hand.  $wd is any empty directory.  $emul is
+	# compat.linux.emul_path (usually /compat/linux).
+	#
+	# Native:
+	#     $wd/tdir/
+	#         Directory.
+	#     $wd/tdir/f
+	#         Regular file containing the word "native".
+	#
+	# ABI (the same path names created under $emul):
+	#     $emul$wd/tdir/
+	#         Empty directory (no file named f).
+	#     $emul$wd/reslink
+	#         Absolute symlink whose target is $wd/tdir.
+	#
+	# A Linux process opens $wd/reslink/f.  namei tries that path
+	# under the ABI root first ($emul$wd/reslink/f), follows
+	# reslink to $wd/tdir, and finds $emul$wd/tdir on the ABI
+	# pass.  The leftover component f is missing there.  Because
+	# the symlink target already resolved under the ABI root,
+	# the native restart must not walk the expansion (that would
+	# incorrectly find $wd/tdir/f).  It restarts the original
+	# path and still gets ENOENT.
+	#
+	# Expected result: ENOENT.
+	#
+	require_linux
+	atf_check mkdir "$abiwd/tdir"
+	atf_check mkdir "$wd/tdir"
+	printf native > "$wd/tdir/f"
+	atf_check ln -s "$wd/tdir" "$abiwd/reslink"
+	atf_check -s not-exit:0 -e ignore "$lsh" -c "cat $wd/reslink/f"
+}
+symlink_target_resolved_suffix_cleanup()
+{
+	cleanup_linux
+}
+
+atf_test_case plain_native_fallback cleanup
+plain_native_fallback_head()
+{
+	atf_set "descr" "Plain path (no symlink) absent under the ABI root" \
+	    "still falls back to the native root"
+	atf_set "require.user" "root"
+}
+plain_native_fallback_body()
+{
+	#
+	# Recreate by hand.  $wd is any empty directory.  $emul is
+	# compat.linux.emul_path (usually /compat/linux).
+	#
+	# Native:
+	#     $wd/f-plain
+	#         Regular file containing the word "native".
+	#
+	# ABI (the same path names created under $emul):
+	#     There is no $emul$wd/f-plain, and no symlink.
+	#
+	# A Linux process opens $wd/f-plain.  namei tries that path
+	# under the ABI root first, gets ENOENT, and restarts the
+	# original path from the native root.
+	#
+	# Expected result: the read returns "native".
+	#
+	require_linux
+	printf native > "$wd/f-plain"
+	atf_check -o inline:"native" "$lsh" -c "cat $wd/f-plain"
+}
+plain_native_fallback_cleanup()
+{
+	cleanup_linux
+}
+
+atf_init_test_cases()
+{
+	atf_add_test_case symlink_target_in_abi_root
+	atf_add_test_case symlink_target_native_only
+	atf_add_test_case symlink_target_native_only_midpath
+	atf_add_test_case symlink_target_resolved_suffix
+	atf_add_test_case plain_native_fallback
+}