Collin Funk <[email protected]> writes:
>> stat -c%m does not seem to work correctly with bind mounts.
>> I have tested the following on Debian Jessie:
>>
>> # mkdir /home/root ; touch /home/zero
>> # mount -o bind /root /home/root
>> # mount -o bind /dev/zero /home/zero
>> # mount -o bind /tmp /tmp
>> # ./src/df -P /home/root /home/zero /tmp
>> Filesystem 1024-blocks Used Available Capacity Mounted on
>> /dev/sda1 7736784 2431152 4889584 34% /home/root
>> udev 10240 0 10240 0% /home/zero
>> /dev/sda1 7736784 2431152 4889584 34% /tmp
>> # ./src/stat -c%m /home/root /home/zero /tmp
>> /
>> /
>> /
>> # ./src/stat --version
>> stat (GNU coreutils) 8.26.4-ca52f
>>
>>
>> The documentation explains:
>>
>> stat outputs the alias for a bind mounted file, rather than the
>> initial mount point of its backing device. One can recursively call
>> stat until there is no change in output, to get the current base mount
>> point
>> (https://www.gnu.org/software/coreutils/manual/html_node/stat-invocation.html#index-bind-mount-1)
>>
>> At least the documentation does not match the result. Note that the
>> documented behaviour would not work for /tmp bind mounted to itself.
>
> It looks like back when this feature implemented mountlist in Gnulib
> used /etc/mtab. Back then, it was very easy to find the alias for a
> mounted file. Using the following setup:
>
> $ mkdir -p /tmp/test
> $ mount -t tmpfs tmpfs /tmp/test
> $ mkdir -p /tmp/test/source /tmp/test/dest
> $ mount --bind /tmp/test/source /tmp/test/dest
>
> Here is what I see on a Debian 6 install:
>
> $ uname -sr
> Linux 2.6.32-5-amd64
> $ src/stat --version | head -n 1
> stat (GNU coreutils) 8.6
> $ src/stat -c %m /tmp/test/dest
> /tmp/test/source
> $ strace src/stat -c %m /tmp/test/dest 2>&1 >/dev/null \
> | grep '^open' | tail -n 1
> open("/etc/mtab", O_RDONLY) = 3
> $ tail -n 1 /etc/mtab
> /tmp/test/source /tmp/test/dest none rw,bind 0 0
> $ tail -n 1 /proc/self/mountinfo
> 23 22 0:17 /source /tmp/test/dest rw,relatime - tmpfs tmpfs rw
>
> That made it very simple to get the alias. However, the format of
> /etc/mtab changed at some point, such that it is no longer easy to get
> the alias. Here is an example from my Fedora 44 machine, where you can
> also see that we use /proc/self/mountinfo nowadays:
>
> $ uname -sr
> Linux 7.1.10-200.fc44.x86_64
> $ stat --version | head -n 1
> stat (GNU coreutils) 9.11.252-aea70
> $ src/stat -c %m /tmp/test/dest
> /tmp/test
> $ strace src/stat -c %m /tmp/test/dest 2>&1 >/dev/null | grep '^open' |
> sed -n 'x;$p'
> openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY|O_CLOEXEC) = 3
> $ tail -n 1 /etc/mtab
> tmpfs /tmp/test/dest tmpfs rw,seclabel,relatime,inode64 0 0
> $ tail -n 1 /proc/self/mountinfo
> 1570 1446 0:143 /source /tmp/test/dest rw,relatime shared:1267 - tmpfs
> tmpfs rw,seclabel,inode64
>
> So, I guess we would have to get the parent mount point and then stat
> the root (i.e., "source" in the above example) relative to it.
>
> Thanks for the detailed report.
I believe these two patches should restore the documented behavior. We
have to use the mount ID and the parent mount ID from
/proc/self/mountinfo to determine if it is a bind mount.
The first patch is for Gnulib and the second is for coreutils. The
version in NEWS is roughly correct. I'm not too sure when the /etc/mtab
format changed, but I know when we started using /proc/self/mountinfo
started being used, so I have used that version:
$ git tag --contains 974c355c595598a219785b6661bb6b030aaef4df \
| sort --version-sort | head -n 1
v8.24
So I have used that. On Debian 6 with coreutils-8.6:
$ mkdir -p /tmp/test
$ mount -t tmpfs tmpfs /tmp/test
$ mkdir -p /tmp/test/source /tmp/test/dest
$ mount --bind /tmp/test/source /tmp/test/dest
$ touch /tmp/test/dest/a
$ mkdir -p /tmp/test/dest/b
$ src/stat -c %m /tmp/test/dest /tmp/test/source \
/tmp/test /tmp/test/dest/a /tmp/test/source/a \
/tmp/test/source/a /tmp/test/source/b
/tmp/test/source
/tmp/test
/tmp/test
/tmp/test
/tmp/test
/tmp/test
/tmp/test
The result is the same on Fedora 44 after applying this patch.
Collin
>From c72a8e54ac2e0978fe6c2ce3fec06ce6af798355 Mon Sep 17 00:00:00 2001
Message-ID: <c72a8e54ac2e0978fe6c2ce3fec06ce6af798355.1788561206.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Fri, 4 Sep 2026 15:26:56 -0700
Subject: [PATCH] mountlist: Save the mount ID and parent mount ID.
These values are read from /proc/self/mountlist and are needed to find
bind mounts on Linux.
* lib/mountlist.h (struct mount_entry): Add the me_mount_id and
me_parent_id fields.
* lib/mountlist.c (read_file_system_list): Set them when reading
/proc/self/mountlist, otherwise set them to -1.
---
ChangeLog | 10 ++++++++++
lib/mountlist.c | 34 +++++++++++++++++++++++++++++++---
lib/mountlist.h | 2 ++
3 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index 3194f35ced..6a3b1ace8c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2026-09-04 Collin Funk <[email protected]>
+
+ mountlist: Save the mount ID and parent mount ID.
+ These values are read from /proc/self/mountlist and are needed to find
+ bind mounts on Linux.
+ * lib/mountlist.h (struct mount_entry): Add the me_mount_id and
+ me_parent_id fields.
+ * lib/mountlist.c (read_file_system_list): Set them when reading
+ /proc/self/mountlist, otherwise set them to -1.
+
2026-09-04 Bruno Haible <[email protected]>
time-h: Fix compilation error in dfa.c on mingw.
diff --git a/lib/mountlist.c b/lib/mountlist.c
index 77ea8cee7e..dd243a7ec5 100644
--- a/lib/mountlist.c
+++ b/lib/mountlist.c
@@ -467,17 +467,19 @@ read_file_system_list (bool need_fs_type)
while (getline (&line, &buf_size, fp) != -1)
{
+ unsigned int mount_id, parent_id;
unsigned int devmaj, devmin;
int rc, mntroot_s;
- rc = sscanf(line, "%*u " /* id - discarded */
- "%*u " /* parent - discarded */
+ rc = sscanf(line, "%u " /* id */
+ "%u " /* parent */
"%u:%u " /* dev major:minor */
"%n", /* mountroot (start) */
+ &mount_id, &parent_id,
&devmaj, &devmin,
&mntroot_s);
- if (rc == 2 || rc == 3) /* 3 if %n included in count. */
+ if (rc == 4 || rc == 5) /* 5 if %n included in count. */
{
/* find end of MNTROOT. */
char *mntroot = line + mntroot_s;
@@ -510,6 +512,8 @@ read_file_system_list (bool need_fs_type)
me = xmalloc (sizeof *me);
+ me->me_mount_id = mount_id;
+ me->me_parent_id = parent_id;
me->me_devname = xstrdup (source);
me->me_mountdir = xstrdup (target);
me->me_mntroot = xstrdup (mntroot);
@@ -562,6 +566,8 @@ read_file_system_list (bool need_fs_type)
bool bind = hasmntopt (mnt, "bind");
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (mnt->mnt_fsname);
me->me_mountdir = xstrdup (mnt->mnt_dir);
me->me_mntroot = NULL;
@@ -593,6 +599,8 @@ read_file_system_list (bool need_fs_type)
char *fs_type = fsp_to_string (fsp);
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (fsp->f_mntfromname);
me->me_mountdir = xstrdup (fsp->f_mntonname);
me->me_mntroot = NULL;
@@ -618,6 +626,8 @@ read_file_system_list (bool need_fs_type)
for (; entries-- > 0; fsp++)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (fsp->f_mntfromname);
me->me_mountdir = xstrdup (fsp->f_mntonname);
me->me_mntroot = NULL;
@@ -710,6 +720,8 @@ read_file_system_list (bool need_fs_type)
break;
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (fi.device_name[0] != '\0'
? fi.device_name : fi.fsh_name);
me->me_mountdir = xstrdup (re != NULL ? re->name : fi.fsh_name);
@@ -760,6 +772,8 @@ read_file_system_list (bool need_fs_type)
for (int counter = 0; counter < numsys; counter++)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (stats[counter].f_mntfromname);
me->me_mountdir = xstrdup (stats[counter].f_mntonname);
me->me_mntroot = NULL;
@@ -790,6 +804,8 @@ read_file_system_list (bool need_fs_type)
while (fread (&mnt, sizeof mnt, 1, fp) > 0)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (mnt.mt_dev);
me->me_mountdir = xstrdup (mnt.mt_filsys);
me->me_mntroot = NULL;
@@ -848,6 +864,8 @@ read_file_system_list (bool need_fs_type)
while ((ret = getextmntent (fp, &mnt, 1)) == 0)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (mnt.mnt_special);
me->me_mountdir = xstrdup (mnt.mnt_mountp);
me->me_mntroot = NULL;
@@ -921,6 +939,8 @@ read_file_system_list (bool need_fs_type)
while ((ret = getmntent (fp, &mnt)) == 0)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (mnt.mnt_special);
me->me_mountdir = xstrdup (mnt.mnt_mountp);
me->me_mntroot = NULL;
@@ -971,6 +991,8 @@ read_file_system_list (bool need_fs_type)
{
struct vmount *vmp = (struct vmount *) thisent;
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
if (vmp->vmt_flags & MNT_REMOTE)
{
char *host, *dir;
@@ -1035,6 +1057,8 @@ read_file_system_list (bool need_fs_type)
if (statvfs (node, &dev) == 0)
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_devname = xstrdup (dev.f_mntfromname);
me->me_mountdir = xstrdup (dev.f_mntonname);
me->me_mntroot = NULL;
@@ -1084,6 +1108,8 @@ read_file_system_list (bool need_fs_type)
fs_name, sizeof fs_name))
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_mountdir = xstrdup (mountdir);
/* Check if drive is remote. See:
<https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea>. */
@@ -1212,6 +1238,8 @@ read_file_system_list (bool need_fs_type)
fs_name, sizeof fs_name))
{
me = xmalloc (sizeof *me);
+ me->me_mount_id = -1;
+ me->me_parent_id = -1;
me->me_mountdir = xstrdup (mountdir);
me->me_remote = false;
/* Here we could use vol_name, something like '\\?\Volume{...}'. */
diff --git a/lib/mountlist.h b/lib/mountlist.h
index de1ff712dd..f1c037a2c5 100644
--- a/lib/mountlist.h
+++ b/lib/mountlist.h
@@ -34,6 +34,8 @@ extern "C" {
/* A mount table entry. */
struct mount_entry
{
+ int me_mount_id; /* Mount ID if found, otherwise -1. */
+ int me_parent_id; /* Parent mount ID if found, otherwise -1. */
char *me_devname; /* Device node name, including "/dev/". */
char *me_mountdir; /* Mount point directory name. */
char *me_mntroot; /* Directory on filesystem of device used */
--
2.55.0
>From 332ed22bb38b966c1b059e5f4697b106a891de59 Mon Sep 17 00:00:00 2001
Message-ID: <332ed22bb38b966c1b059e5f4697b106a891de59.1788561208.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Fri, 4 Sep 2026 15:11:32 -0700
Subject: [PATCH] stat: emit the alias for bind mount points
* src/stat.c: Include filenamecat.h
(find_bind_mount): Add a parameter to track whether the result should be
freed. Don't return a const pointer. Find the bind mount point from the
mount ID and parent mount ID.
(out_mount_point): Adjust the callers. Free the result if needed.
* NEWS: Mention the bug fix.
Fixes https://bugs.gnu.org/25173
---
NEWS | 4 ++++
src/stat.c | 55 +++++++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/NEWS b/NEWS
index e52e8b7ce..53d2002a4 100644
--- a/NEWS
+++ b/NEWS
@@ -64,6 +64,10 @@ GNU coreutils NEWS -*- outline -*-
'shred' no longer blocks when opening a FIFO that has no readers.
[This bug was present in "the beginning".]
+ 'stat --format %m MOUNT' now prints the alias for MOUNT if it is a
+ bind mounted file instead of the initial mount point.
+ [bug introduced in coreutils-8.24]
+
'stty' no longer fails when the system uses speed encodings that
are variations of the requested speed.
[bug introduced in coreutils-9.8]
diff --git a/src/stat.c b/src/stat.c
index d3674294a..df4fe3649 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -62,6 +62,7 @@
#include "c-ctype.h"
#include "file-type.h"
#include "filemode.h"
+#include "filenamecat.h"
#include "fs.h"
#include "mountlist.h"
#include "octhexdigits.h"
@@ -964,13 +965,16 @@ print_statfs (char *pformat, size_t prefix_len, MAYBE_UNUSED char mod, char m,
}
/* Return any bind mounted source for a path.
- The caller should not free the returned buffer.
+ The caller should not free the returned buffer unless MALLOC_RESULT
+ is true.
Return NULL if no bind mount found. */
NODISCARD
-static char const *
-find_bind_mount (char const * name)
+static char *
+find_bind_mount (char const *name, bool *malloced_result)
{
- char const * bind_mount = NULL;
+ char *bind_mount = NULL;
+
+ *malloced_result = false;
static struct mount_entry *mount_list;
static bool tried_mount_list = false;
@@ -987,20 +991,49 @@ find_bind_mount (char const * name)
for (struct mount_entry *me = mount_list; me; me = me->me_next)
{
- if (me->me_dummy && me->me_devname[0] == '/'
+ if (0 <= me->me_mount_id && 0 <= me->me_parent_id
&& streq (me->me_mountdir, name))
{
+ /* We are using /proc/self/mountinfo. */
+ for (struct mount_entry *parent = mount_list; parent;
+ parent = parent->me_next)
+ {
+ if (parent->me_mount_id == me->me_parent_id
+ && parent->me_dev == me->me_dev)
+ {
+ struct stat resolved_stats;
+ char *resolved_name
+ = file_name_concat (parent->me_mountdir, me->me_mntroot,
+ NULL);
+
+ if (!(stat (resolved_name, &resolved_stats) == 0
+ && psame_inode (&name_stats, &resolved_stats)))
+ free (resolved_name);
+ else
+ {
+ bind_mount = resolved_name;
+ *malloced_result = true;
+ goto out;
+ }
+ }
+ }
+ }
+ else if (me->me_mount_id < 0 && me->me_parent_id < 0 && me->me_dummy
+ && me->me_devname[0] == '/' && streq (me->me_mountdir, name))
+ {
+ /* We are using /etc/mtab. */
struct stat dev_stats;
if (stat (me->me_devname, &dev_stats) == 0
&& psame_inode (&name_stats, &dev_stats))
{
bind_mount = me->me_devname;
- break;
+ goto out;
}
}
}
+ out:
return bind_mount;
}
@@ -1011,9 +1044,11 @@ out_mount_point (char const *filename, char *pformat, size_t prefix_len,
const struct stat *statp)
{
- char const *np = "?", *bp = NULL;
+ char const *np = "?";
+ char *bp = NULL;
char *mp = NULL;
bool fail = true;
+ bool malloced_bp;
/* Look for bind mounts first. Note we output the immediate alias,
rather than further resolving to a base device mount point. */
@@ -1025,7 +1060,7 @@ out_mount_point (char const *filename, char *pformat, size_t prefix_len,
error (0, errno, _("failed to canonicalize %s"), quoteaf (filename));
goto print_mount_point;
}
- bp = find_bind_mount (resolved);
+ bp = find_bind_mount (resolved, &malloced_bp);
free (resolved);
if (bp)
{
@@ -1043,13 +1078,15 @@ out_mount_point (char const *filename, char *pformat, size_t prefix_len,
{
/* This dir might be bind mounted to another device,
so we resolve the bound source in that case also. */
- bp = find_bind_mount (mp);
+ bp = find_bind_mount (mp, &malloced_bp);
fail = false;
}
print_mount_point:
out_string (pformat, prefix_len, bp ? bp : mp ? mp : np);
+ if (malloced_bp)
+ free (bp);
free (mp);
return fail;
}
--
2.55.0