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

Reply via email to