Re: [PATCH v3 00/12] man2: document "new" mount API

From: Askar Safin
Date: Sun Aug 17 2025 - 14:52:10 EST


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);
}