* src/chcon.c: Include fcntl--.h.
(change_file_context): Open a file descriptor when only a partial
context is specified. Use it to get and subsequently set the file
context, instead of using getfileconat followed by setfileconat.
* tests/chcon/traversal-missing.sh: Adjust the test using partial
contexts to inject open and openat errors. Add a test for complete
contexts that injects errors on xattr system calls.
* NEWS: Mention the bug fix.
Discovered by Anthropic's OSS Scanner, as bug ANT-2026-9PFL6OLT.
---
NEWS | 4 +++
src/chcon.c | 57 +++++++++++++++++++++-----------
tests/chcon/traversal-missing.sh | 35 ++++++++++++++++----
3 files changed, 70 insertions(+), 26 deletions(-)
diff --git a/NEWS b/NEWS
index 02bb0d05c..c22547130 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,10 @@ GNU coreutils NEWS -*-
outline -*-
security context fails instead of the basename.
[bug introduced with the chcon program in coreutils-6.9.90]
+ 'chcon' avoids a race between getting and setting security contexts of a file
+ when only a partial context is specified.
+ [bug introduced with the chcon program in coreutils-6.9.90]
+
'chroot --userspec=USER' now uses the primary group of USER as looked up
inside the chroot. Previously the group looked up outside the chroot
was used, even when the look-up inside the chroot succeeded.
diff --git a/src/chcon.c b/src/chcon.c
index 4b0e71ed3..cadf5ace3 100644
--- a/src/chcon.c
+++ b/src/chcon.c
@@ -22,6 +22,7 @@
#include "system.h"
#include "dev-ino.h"
+#include "fcntl--.h"
#include "fts-missing.h"
#include "ignore-value.h"
#include "quote.h"
@@ -138,26 +139,41 @@ compute_context_from_mask (char const *context, context_t
*ret)
Return 0 if successful, 1 if errors occurred. */
static int
-change_file_context (char const *file_full_name, int fd, char const *file,
- bool ignore_missing)
+change_file_context (char const *file_full_name, int const dirfd,
+ char const *file, bool ignore_missing)
{
char *file_context = NULL;
context_t context IF_LINT (= NULL);
char const * context_string;
int errors = 0;
+ int fd = -1;
if (specified_context == NULL)
{
- int status = (affect_symlink_referent
- ? getfileconat (fd, file, &file_context)
- : lgetfileconat (fd, file, &file_context));
-
- if (status < 0 && errno != ENODATA)
+ /* libselinux versions before 3.4 did not support O_PATH.
+ The O_PATH support works by opening the file descriptor under
+ /proc/self/fd. We could check if fsetfilecon returns EBADF and
+ do the same, but there is likely not much benefit over just
+ using O_RDONLY. */
+ fd = openat (dirfd, file,
+ (O_RDONLY | O_NOCTTY | O_NONBLOCK
+ | (affect_symlink_referent ? 0 : O_NOFOLLOW)));
+
+ if (fd < 0)
{
if (ignore_missing && ignorable_traversal_errno (errno))
return 0;
+ error (0, errno, _("failed to open %s"), quoteaf (file_full_name));
+ return 1;
+ }
+
+ int const status = fgetfilecon (fd, &file_context);
+
+ if (status < 0 && errno != ENODATA)
+ {
error (0, errno, _("failed to get security context of %s"),
quoteaf (file_full_name));
+ close (fd);
return 1;
}
@@ -168,12 +184,14 @@ change_file_context (char const *file_full_name, int fd,
char const *file,
{
error (0, 0, _("can't apply partial context to unlabeled file %s"),
quoteaf (file_full_name));
+ close (fd);
return 1;
}
if (compute_context_from_mask (file_context, &context))
{
freecon (file_context);
+ close (fd);
return 1;
}
@@ -186,19 +204,19 @@ change_file_context (char const *file_full_name, int fd,
char const *file,
if (file_context == NULL || ! streq (context_string, file_context))
{
- int fail = (affect_symlink_referent
- ? setfileconat (fd, file, context_string)
- : lsetfileconat (fd, file, context_string));
-
- if (fail)
+ int fail = (0 <= fd
+ ? fsetfilecon (fd, context_string)
+ : affect_symlink_referent
+ ? setfileconat (dirfd, file, context_string)
+ : lsetfileconat (dirfd, file, context_string));
+
+ if (fail
+ && ! (fd < 0 && ignore_missing && ignorable_traversal_errno (errno)))
{
- if (! (ignore_missing && ignorable_traversal_errno (errno)))
- {
- errors = 1;
- error (0, errno, _("failed to change context of %s to %s"),
- quoteaf_n (0, file_full_name),
- quote_n (1, context_string));
- }
+ errors = 1;
+ error (0, errno, _("failed to change context of %s to %s"),
+ quoteaf_n (0, file_full_name),
+ quote_n (1, context_string));
}
}
@@ -206,6 +224,7 @@ change_file_context (char const *file_full_name, int fd,
char const *file,
{
context_free (context);
freecon (file_context);
+ close (fd);
}
return errors;
diff --git a/tests/chcon/traversal-missing.sh b/tests/chcon/traversal-missing.sh
index 7286cc8fd..6a0e912dd 100755
--- a/tests/chcon/traversal-missing.sh
+++ b/tests/chcon/traversal-missing.sh
@@ -20,22 +20,24 @@
print_ver_ chcon
require_strace_ getxattr
-mkdir d || framework_failure_
-touch d/foo || framework_failure_
-xattrs=getxattr,lgetxattr,setxattr,lsetxattr
+mkdir -p d/foo || framework_failure_
+touch d/foo/bar || framework_failure_
+opens=open,openat
(cd d && chcon -R -t user_tmp_t . > /dev/null 2>&1) \
|| skip_ 'chcon does not work on the test file system'
# Skip if strace does not support path-filtered syscall injection.
-strace --quiet=all -o /dev/null -P /proc/self/fd/4/foo \
- -e inject=$xattrs:error=ENOENT true 2> /dev/null \
+strace --quiet=all -o /dev/null -P d/foo -P d/foo/bar \
+ -e inject=$opens:error=ENOENT true 2> /dev/null \
|| skip_ 'strace does not support the required options and syscalls'
+# Test traversal using a partial context, where we use openat to
+# avoid races.
for errnum in ENOENT; do
rm -f err trace
- (cd d && strace --quiet=all -o ../trace -P /proc/self/fd/4/foo \
- -e inject=$xattrs:error=$errnum chcon -R -t user_tmp_t . \
+ (cd d && strace --quiet=all -o ../trace -P foo -P foo/bar \
+ -e inject=$opens:error=$errnum chcon -R -t user_tmp_t . \
> /dev/null 2> ../err)
status=$?
@@ -45,4 +47,23 @@ for errnum in ENOENT; do
test ! -s err || { cat err; fail=1; }
done
+# Test traversal on a complete context, where getfileconat
+# or lgetfileconat are used. This requires root, and we use
+# root:object_r:tmp_t:s0 since it works without mcstransd running.
+if uid_is_privileged_; then
+ xattrs=getxattr,lgetxattr,setxattr,lsetxattr
+ for errnum in ENOENT; do
+ rm -f err trace
+ (cd d && strace --quiet=all -o ../trace -P /proc/self/fd/4/foo \
+ -P /proc/self/fd/5/bar -e inject=$xattrs:error=$errnum \
+ chcon -R root:object_r:tmp_t:s0 . > /dev/null 2> ../err)
+ status=$?
+
+ grep ' (INJECTED)' trace > /dev/null 2>&1 \
+ || skip_ 'strace did not inject the requested failure'
+ test $status -eq 0 || fail=1
+ test ! -s err || { cat err; fail=1; }
+ done
+fi
+
Exit $fail
--
2.56.0