* src/df.c (automount_stat_err): Use O_DIRECTORY when opening operands.
Fall back to stat if open fails with ENOTDIR.
* tests/df/open.sh: New file.
* tests/local.mk (all_tests): Add the new test case.
* NEWS: Mention the bug fix.
Discovered by Anthropic's OSS Scanner, as bug ANT-2026-Z5SPLRCM.
---
NEWS | 5 +++++
src/df.c | 4 ++--
tests/df/open.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++++
tests/local.mk | 1 +
4 files changed, 64 insertions(+), 2 deletions(-)
create mode 100755 tests/df/open.sh
diff --git a/NEWS b/NEWS
index 3361336ef..18cea7900 100644
--- a/NEWS
+++ b/NEWS
@@ -85,6 +85,11 @@ GNU coreutils NEWS -*-
outline -*-
oflag=seek_bytes, and when N is smaller than the output block size.
[bug introduced with oflag=seek_bytes in coreutils-8.16]
+ 'df' no longer opens non-directory operands. Previously, it would open and
+ subsequently close them, which may lead to unintended behavior when the
+ operand is a device or a symbolic link to a device.
+ [bug introduced in coreutils-7.3]
+
'dircolors --csh' now escapes '!' characters in its output. Previously, csh
would perform history substitution on the output when evaluated, which would
fail or execute arbitrary commands depending on the contents of the database
diff --git a/src/df.c b/src/df.c
index 969d09aab..005aa3fe1 100644
--- a/src/df.c
+++ b/src/df.c
@@ -288,10 +288,10 @@ static struct option const long_options[] =
static int
automount_stat_err (char const *file, struct stat *st)
{
- int fd = open (file, O_RDONLY | O_NOCTTY | O_NONBLOCK);
+ int fd = open (file, O_RDONLY | O_DIRECTORY | O_NOCTTY | O_NONBLOCK);
if (fd < 0)
{
- if (errno == ENOENT || errno == ENOTDIR)
+ if (errno == ENOENT)
return errno;
return stat (file, st) == 0 ? 0 : errno;
}
diff --git a/tests/df/open.sh b/tests/df/open.sh
new file mode 100755
index 000000000..29f8d5be6
--- /dev/null
+++ b/tests/df/open.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+# Test that 'df' opens directory operands to trigger automounts.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program 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 3 of the License, or
+# (at your option) any later version.
+
+# This program 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.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ df
+require_strace_ openat
+getlimits_
+
+trace_open ()
+{
+ strace --quiet=all -o trace -e trace=open,openat "$@" 2>err
+}
+
+trace_open true || skip_ 'strace does not support the required options'
+
+ln -s /dev/null dev_null &&
+touch file || framework_failure_
+
+# Make sure that non-directories don't get opened.
+for file in file dev_null; do
+ trace_open df $file || fail=1
+ compare /dev/null err || fail=1
+ returns_ 1 grep '^open.*"'$file'",.* = [0-9]' trace || fail=1
+done
+
+# Make sure that directories get opened so that automounts get triggered.
+mkdir dir || framework_failure_
+trace_open df dir || fail=1
+compare /dev/null err || fail=1
+grep '^open.*"dir",.* = [0-9]' trace || fail=1
+
+# Make sure the errors given for missing files are what we expect.
+cat <<EOF >exp-err || framework_failure_
+df: missing: $ENOENT
+df: missing/f: $ENOENT
+EOF
+returns_ 1 df missing missing/f >out 2>err || fail=1
+compare /dev/null out || fail=1
+compare exp-err err || fail=1
+
+Exit $fail
diff --git a/tests/local.mk b/tests/local.mk
index 0a49022a9..3364a543e 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -656,6 +656,7 @@ all_tests = \
tests/df/df-P.sh \
tests/df/df-output.sh \
tests/df/df-symlink.sh \
+ tests/df/open.sh \
tests/df/sync.sh \
tests/df/unreadable.sh \
tests/df/total-unprocessed.sh \
--
2.56.0