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>
{
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);
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"}
--- /dev/null
+inode0: OK
+inode1: OK
--- /dev/null
+#!/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