]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
lsfd: (bugfix) do not reuse stat(2) buffer for files with identical names
authorMasatake YAMATO <yamato@redhat.com>
Thu, 22 Jan 2026 16:19:09 +0000 (01:19 +0900)
committerMasatake YAMATO <yamato@redhat.com>
Wed, 28 Jan 2026 13:32:14 +0000 (22:32 +0900)
To reduce the number of stat(2) calls, lsfd reused the buffer returned
from stat(2) when file descriptors opened files with the same name.

If file descriptors open different files that happen to have the same
name, lsfd may report incorrect results. In such cases, the stat(2)
buffer must not be reused.

The program a.out is run with a file name "D/a".  It opens the file
twice during its execution with an interval.  Between the two open()
calls, a different filesystem is mounted on "D".

<the source code of ./a.out>

  #include <fcntl.h>
  #include <err.h>
  #include <string.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>

  static void usage(const char *prog, int eval, FILE *fp)
  {
    fputs("Usage :\n", fp);
    fprintf(fp, " %s FILE\n", prog);
    exit(eval);
  }

  int main(int argc, char **argv)
  {
    const char *fname;
    int fd0, fd1;

    if (argc < 2)
      errx(2, "too few arguements");
    if (strcmp(argv[1], "-h") == 0 ||
strcmp(argv[1], "--h") == 0)
      usage(argv[0], 0, stdout);
    if (argc > 2)
      errx(2, "too many arguements");

    fname = argv[1];

    printf("pid: %d\n", getpid());
    fd0 = open(fname, O_RDONLY);
    if (fd0 < 0)
      err(1, "error in open \"%s\" in the first time", fname);

    fputs("[press RETURN to go to the next step] ", stderr);
    getchar();

    fd1 = open(fname, O_RDONLY);
    if (fd1 < 0)
      err(1, "error in open \"%s\" in the second time", fname);

    fputs("[press RETURN to exit] ", stderr);
    getchar();

    return 0;
  }

<PREPARATION>
  $ mkdir D
  $ touch D/a
  $ dd if=/dev/zero of=img.xfs count=1 bs=400MB
  1+0 records in
  1+0 records out
  400000000 bytes (400 MB, 381 MiB) copied, 0.427125 s, 936 MB/s
  $ mkfs.xfs img.xfs
  meta-data=img.xfs                isize=512    agcount=4, agsize=24414 blks
   =                       sectsz=512   attr=2, projid32bit=1
   =                       crc=1        finobt=1, sparse=1, rmapbt=1
   =                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
   =                       exchange=0   metadir=0
  data     =                       bsize=4096   blocks=97656, imaxpct=25
   =                       sunit=0      swidth=0 blks
  naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
  log      =internal log           bsize=4096   blocks=16384, version=2
   =                       sectsz=512   sunit=0 blks, lazy-count=1
  realtime =none                   extsz=4096   blocks=0, rtextents=0
   =                       rgcount=0    rgsize=0 extents
   =                       zoned=0      start=0 reserved=0
  $ sudo mount img.xfs D
  $ sudo touch D/a
  $ sudo umount D

Let's see the bug.

<TERMNAL-1>
  $ ./a.out D/a
  pid: 770257
  [press RETURN to go to the next step]

<TERMNAL-2>
  $ sudo mount img.xfs D

<TERMNAL-1>
  (press RETURN)
  [press RETURN to exit]

<TERMNAL-2>
  $ ./lsfd-orignal -p 770257 -Q 'FD > 2'
  COMMAND    PID   USER ASSOC  XMODE TYPE SOURCE MNTID    INODE NAME
  a.out   770257 yamato     3 r-----  REG   dm-3    95 30947324 /home/yamato/D/a
  a.out   770257 yamato     4 r-----  REG   dm-3  1631 30947324 /home/yamato/D/a

  $ stat D/a
    File: D/a
    Size: 0               Blocks: 0          IO Block: 4096   regular empty file
  Device: 7,10    Inode: 131         Links: 1
  ...

Although D/a has inode number 131, lsfd-original reports 30947324.
After removing the code that reuses the stat(2) buffer, lsfd-new reports:

<TERMNAL-2>
  $ ./lsfd-new -p 770257 -Q 'FD > 2'
  COMMAND    PID   USER ASSOC  XMODE TYPE SOURCE MNTID    INODE NAME
  a.out   770257 yamato     3 r-----  REG   dm-3    95 30947324 /home/yamato/D/a
  a.out   770257 yamato     4 r-----  REG loop10  1631      131 /home/yamato/D/a

Signed-off-by: Masatake YAMATO <yamato@redhat.com>
lsfd-cmd/lsfd.c
tests/commands.sh
tests/expected/lsfd/mount-over-same-path [new file with mode: 0644]
tests/ts/lsfd/mount-over-same-path [new file with mode: 0755]

index 0776ecfbc4d3797010a89daf7f1e9816c727c721..1326082d5eadbd7e2b6dc0cd5c29c815905d10e9 100644 (file)
@@ -886,20 +886,11 @@ static struct file *collect_file_symlink(struct path_cxt *pc,
 {
        char sym[PATH_MAX] = { '\0' };
        struct stat sb = { .st_mode = 0 };
-       struct file *f, *prev;
+       struct file *f;
 
        if (ul_path_readlink(pc, sym, sizeof(sym), name) < 0)
                f = new_readlink_error_file(proc, errno, assoc);
-       /* The /proc/#/{fd,ns} often contains the same file (e.g. /dev/tty)
-        * more than once. Let's try to reuse the previous file if the real
-        * path is the same to save stat() call.
-        */
-       else if ((prev = list_last_entry(&proc->files, struct file, files))
-                && (!is_error_object(prev))
-                && prev->name && strcmp(prev->name, sym) == 0) {
-               f = copy_file(prev, assoc);
-               sb = prev->stat;
-       } else if (ul_path_stat(pc, &sb, 0, name) < 0)
+       else if (ul_path_stat(pc, &sb, 0, name) < 0)
                f = new_stat_error_file(proc, sym, errno, assoc);
        else {
                const struct file_class *class = stat2class(&sb);
index 7b0c088a26e2b784da2bfe0e29713e39ceb53163..f6d8a43c83707b7828adab316e6691e952bfa10c 100644 (file)
@@ -67,6 +67,7 @@ TS_HELPER_PROCFS="${ts_helpersdir}test_procfs"
 TS_HELPER_TIMEUTILS="${ts_helpersdir}test_timeutils"
 TS_HELPER_KILL_PIDFDINO="${ts_helpersdir}test_kill_pidfdino"
 TS_HELPER_SCOLS_TERMREDUCE="${ts_helpersdir}test_scols_termreduce"
+TS_HELPER_OPEN_TWICE="${ts_helpersdir}test_open_twice"
 
 # paths to commands
 TS_CMD_ADDPART=${TS_CMD_ADDPART:-"${ts_commandsdir}addpart"}
diff --git a/tests/expected/lsfd/mount-over-same-path b/tests/expected/lsfd/mount-over-same-path
new file mode 100644 (file)
index 0000000..40ee457
--- /dev/null
@@ -0,0 +1,2 @@
+inode0: OK
+inode1: OK
diff --git a/tests/ts/lsfd/mount-over-same-path b/tests/ts/lsfd/mount-over-same-path
new file mode 100755 (executable)
index 0000000..bc29793
--- /dev/null
@@ -0,0 +1,108 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Masatake YAMATO <yamato@redhat.com>
+#
+# This file is part of util-linux.
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This file is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="open files with same path across mount over"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+ts_skip_nonroot
+
+ts_check_test_command "$TS_CMD_LSFD"
+
+ts_check_test_command "$TS_CMD_MOUNT"
+ts_check_test_command "$TS_CMD_UMOUNT"
+ts_check_test_command "$TS_HELPER_OPEN_TWICE"
+
+ts_check_losetup
+
+ts_check_prog "mkfs.ext2"
+ts_check_prog "rm"
+ts_check_prog "rmdir"
+ts_check_prog "stat"
+ts_check_prog "touch"
+
+ts_cd "$TS_OUTDIR"
+
+ts_device_init 1
+DEVICE=$TS_LODEV
+
+mkfs.ext2 "$DEVICE" &> /dev/null || ts_die "Cannot make ext2 on $DEVICE"
+# Let's wait for udev scanning the device modification.
+ts_udevadm_settle "$DEVICE"
+
+if ! ts_device_has "TYPE" "ext2" "$DEVICE"; then
+    ts_die "Cannot find ext2 on $DEVICE"
+fi
+
+[ -d "$TS_MOUNTPOINT" ] || mkdir -p "$TS_MOUNTPOINT"
+
+if ! touch "$TS_MOUNTPOINT"/a; then
+    ts_failed "$TS_MOUNTPOINT/a: cannot make the file"
+fi
+
+if ! "$TS_CMD_MOUNT" "$DEVICE" "$TS_MOUNTPOINT"; then
+    ts_failed "$DEVICE: cannot mount the fs on the device(prep)"
+fi
+
+if ! touch "$TS_MOUNTPOINT"/a; then
+    ts_failed "$TS_MOUNTPOINT/a: cannot make the file"
+fi
+
+if ! "$TS_CMD_UMOUNT" -q "$TS_MOUNTPOINT"; then
+    ts_failed "$DEVICE: cannot umount the fs"
+fi
+
+INODE0=$(stat -c %i "$TS_MOUNTPOINT/a")
+coproc MKFDS { "$TS_HELPER_OPEN_TWICE" "$TS_MOUNTPOINT"/a; }
+if read -r -u "${MKFDS[0]}" PID FD0; then
+    if inode0=$(${TS_CMD_LSFD} --noheadings --raw --pid "$PID" --filter "FD == $FD0" -oINODE); then
+       if [[ -n "$INODE0" && "$INODE0" == "$inode0" ]]; then
+           echo inode0: OK >> "$TS_OUTPUT"
+       else
+           echo inode0: FAILED "$INODE0" == "$inode0" >> "$TS_OUTPUT"
+
+           echo >&"${MKFDS[1]}"
+           echo >&"${MKFDS[1]}"
+           ts_failed "inode doesn't match in the first file open"
+       fi
+
+       if ! "$TS_CMD_MOUNT" "$DEVICE" "$TS_MOUNTPOINT"; then
+           echo >&"${MKFDS[1]}"
+           echo >&"${MKFDS[1]}"
+           ts_failed "$DEVICE: cannot mount the fs on the device(fd1)"
+       fi
+       INODE1=$(stat -c %i "$TS_MOUNTPOINT"/a)
+
+       echo >&"${MKFDS[1]}"
+       if read -r -u "${MKFDS[0]}" FD1; then
+           if inode1=$(${TS_CMD_LSFD} --noheadings --raw --pid "$PID" --filter "FD == $FD1" -oINODE); then
+               if [[ -n "$INODE1" && "$INODE1" == "$inode1" ]]; then
+                   echo inode1: OK >> $TS_OUTPUT
+               else
+                   echo inode1: FAILED "$INODE1" == "$inode1" >> $TS_OUTPUT
+
+                   echo >&"${MKFDS[1]}"
+                   ts_failed "inode doesn't match in the second file open"
+               fi
+           fi
+       fi
+       echo >&"${MKFDS[1]}"
+    fi
+    wait "${MKFDS_PID}"
+fi
+
+ts_finalize