I just sent to fsdevel fix for that RESOLVE_NO_XDEV bug. Aleksa Sarai <cyphar@xxxxxxxxxx>: > No, LOOKUP_AUTOMOUNT affects all components. I double-checked this with > Christian. No. I just tested this. See tests (and miniconfig) in the end of this message. statx always follows automounts in non-final components no matter what. I tested this. And it follows automounts in final component depending on AT_NO_AUTOMOUNT. I tested this too. Also, absolutely all other syscalls always follow automounts in non-final components no matter what. With sole exception for openat2 with RESOLVE_NO_XDEV. I didn't test this, but I conclude this by reading code. First of all, LOOKUP_PARENT's doc in kernel currently is wrong: https://elixir.bootlin.com/linux/v6.17-rc1/source/include/linux/namei.h#L31 We see there: #define LOOKUP_PARENT BIT(10) /* Looking up final parent in path */ This is not true. LOOKUP_PARENT means that we are resolving any non-final component. LOOKUP_PARENT is set when we enter link_path_walk, which is used for resolving everything except for final component. And LOOKUP_PARENT is cleared when we leave link_path_walk. Now let's look here: https://elixir.bootlin.com/linux/v6.17-rc1/source/fs/namei.c#L1447 if (!(lookup_flags & (LOOKUP_PARENT | LOOKUP_DIRECTORY | LOOKUP_OPEN | LOOKUP_CREATE | LOOKUP_AUTOMOUNT)) && We never return -EISDIR in this "if" if we are in non-final component thanks to LOOKUP_PARENT here. We fall to finish_automount instead. Again: if this is non-final component, then LOOKUP_PARENT is set, and thus LOOKUP_AUTOMOUNT is ignored. If this is final component, then LOOKUP_AUTOMOUNT may affect things. Code below tests that: - statx always follows non-final automounts - statx follow final automounts depending on options The code doesn't test other syscalls, they can be added if needed. The code was tested in Qemu on Linux 6.17-rc1. I'm not trying to insult you in any way. Again: thank you a lot for your work! For openat2 and for these mans. Askar Safin ==== miniconfig: CONFIG_64BIT=y CONFIG_EXPERT=y CONFIG_PRINTK=y CONFIG_PRINTK_TIME=y CONFIG_TTY=y CONFIG_VT=y CONFIG_VT_CONSOLE=y CONFIG_FRAMEBUFFER_CONSOLE=y CONFIG_PROC_FS=y CONFIG_DEVTMPFS=y CONFIG_SYSFS=y CONFIG_TMPFS=y CONFIG_DEBUG_FS=y CONFIG_USER_EVENTS=y CONFIG_FTRACE=y CONFIG_MULTIUSER=y CONFIG_NAMESPACES=y CONFIG_USER_NS=y CONFIG_PID_NS=y CONFIG_SERIAL_8250=y CONFIG_SERIAL_8250_CONSOLE=y CONFIG_BLK_DEV_INITRD=y CONFIG_RD_GZIP=y CONFIG_BINFMT_ELF=y CONFIG_BINFMT_SCRIPT=y CONFIG_TRACEFS_AUTOMOUNT_DEPRECATED=y CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y ==== /* Author: Askar Safin Public domain Make sure your kernel is compiled with CONFIG_TRACEFS_AUTOMOUNT_DEPRECATED=y If all tests pass, the program should print "All tests passed". Any other output means that something gone wrong. This program requires root in initial user namespace */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sched.h> #include <errno.h> #include <sys/stat.h> #include <sys/mount.h> #include <sys/syscall.h> #include <linux/openat2.h> #define MY_ASSERT(cond) do { \ if (!(cond)) { \ fprintf (stderr, "%s: assertion failed\n", #cond); \ exit (1); \ } \ } while (0) bool tracing_mounted (void) { struct statx tracing; if (statx (AT_FDCWD, "/tmp/debugfs/tracing", AT_NO_AUTOMOUNT, 0, &tracing) != 0) { perror ("statx tracing"); exit (1); } if (!(tracing.stx_attributes_mask & STATX_ATTR_MOUNT_ROOT)) { fprintf (stderr, "???\n"); exit (1); } return tracing.stx_attributes & STATX_ATTR_MOUNT_ROOT; } void mount_debugfs (void) { if (mount (NULL, "/tmp/debugfs", "debugfs", 0, NULL) != 0) { perror ("mount debugfs"); exit (1); } MY_ASSERT (!tracing_mounted ()); } void umount_debugfs (void) { umount ("/tmp/debugfs/tracing"); // Ignore errors if (umount ("/tmp/debugfs") != 0) { perror ("umount debugfs"); exit (1); } } int main (void) { // Init { if (chdir ("/") != 0) { perror ("chdir /"); exit (1); } if (unshare (CLONE_NEWNS) != 0) { perror ("unshare"); exit (1); } if (mount (NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) { perror ("mount(NULL, /, NULL, MS_REC | MS_PRIVATE, NULL)"); exit (1); } if (mount (NULL, "/tmp", "tmpfs", 0, NULL) != 0) { perror ("mount tmpfs"); exit (1); } } if (mkdir ("/tmp/debugfs", 0777) != 0) { perror ("mkdir(/tmp/debugfs)"); exit (1); } // statx always follows automounts in non-final components. With AT_NO_AUTOMOUNT and without AT_NO_AUTOMOUNT { mount_debugfs(); { struct statx readme; if (statx (AT_FDCWD, "/tmp/debugfs/tracing/README", 0, 0, &readme) != 0) { perror ("statx"); exit (1); } } MY_ASSERT (tracing_mounted ()); umount_debugfs(); mount_debugfs(); { struct statx readme; if (statx (AT_FDCWD, "/tmp/debugfs/tracing/README", AT_NO_AUTOMOUNT, 0, &readme) != 0) { perror ("statx"); exit (1); } } MY_ASSERT (tracing_mounted ()); umount_debugfs(); } // statx follows automounts in final components if AT_NO_AUTOMOUNT is not specified { mount_debugfs(); { struct statx tracing; if (statx (AT_FDCWD, "/tmp/debugfs/tracing", 0, 0, &tracing) != 0) { perror ("statx"); exit (1); } if (!(tracing.stx_attributes_mask & STATX_ATTR_MOUNT_ROOT)) { fprintf (stderr, "???\n"); exit (1); } // Checking that this is new mount, not automount point itself MY_ASSERT (tracing.stx_attributes & STATX_ATTR_MOUNT_ROOT); } MY_ASSERT (tracing_mounted ()); umount_debugfs (); mount_debugfs(); { struct statx tracing; if (statx (AT_FDCWD, "/tmp/debugfs/tracing", AT_NO_AUTOMOUNT, 0, &tracing) != 0) { perror ("statx"); exit (1); } if (!(tracing.stx_attributes_mask & STATX_ATTR_MOUNT_ROOT)) { fprintf (stderr, "???\n"); exit (1); } MY_ASSERT (!(tracing.stx_attributes & STATX_ATTR_MOUNT_ROOT)); } MY_ASSERT (!tracing_mounted ()); umount_debugfs (); } printf ("All tests passed\n"); exit (0); }