From 774ee26f969f3fadd4d3bb47ae218367a2bae0b9 Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Tue, 21 Apr 2026 14:01:42 +0200 Subject: [PATCH] Address a potential TOCTOU race condition in cap_set_file(). Backport of upstream commit 286ace1259992bd0c5d9016715833f2e148ac596 from https://git.kernel.org/pub/scm/libs/libcap/libcap.git This issue was researched and reported by Ali Raza (@locus-x64). It has been assigned CVE-2026-4878. The finding is that while cap_set_file() checks if a file is a regular file before applying or removing a capability attribute, a small window existed after that check when the filepath could be overwritten either with new content or a symlink to some other file. To do this would imply that the caller of cap_set_file() was directing it to a directory over which a local attacker has write access, and performed the operation frequently enough that an attacker had a non-negligible chance of exploiting the race condition. The code now locks onto the intended file, eliminating the race condition. Signed-off-by: Anderson Toshiyuki Sasaki --- libcap/cap_file.c | 69 +++++++++++++++++++++++++++++++++++++++------- progs/quicktest.sh | 14 +++++++++- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/libcap/cap_file.c b/libcap/cap_file.c index 84ae3e1..911a21d 100644 --- a/libcap/cap_file.c +++ b/libcap/cap_file.c @@ -8,8 +8,13 @@ #define _DEFAULT_SOURCE #endif +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + #include #include +#include #include #include #include @@ -314,26 +319,70 @@ int cap_set_file(const char *filename, cap_t cap_d) struct vfs_ns_cap_data rawvfscap; int sizeofcaps; struct stat buf; + char fdpath[64]; + int fd, ret; + + _cap_debug("setting filename capabilities"); + fd = open(filename, O_RDONLY|O_NOFOLLOW); + if (fd >= 0) { + ret = cap_set_fd(fd, cap_d); + close(fd); + return ret; + } - if (lstat(filename, &buf) != 0) { - _cap_debug("unable to stat file [%s]", filename); + /* + * Attempting to set a file capability on a file the process can't + * read the content of. This is considered a non-standard use case + * and the following (slower) code is complicated because it is + * trying to avoid a TOCTOU race condition. + */ + + fd = open(filename, O_PATH|O_NOFOLLOW); + if (fd < 0) { + _cap_debug("cannot find file at path [%s]", filename); + return -1; + } + if (fstat(fd, &buf) != 0) { + _cap_debug("unable to stat file [%s] descriptor %d", + filename, fd); + close(fd); return -1; } if (S_ISLNK(buf.st_mode) || !S_ISREG(buf.st_mode)) { - _cap_debug("file [%s] is not a regular file", filename); + _cap_debug("file [%s] descriptor %d for non-regular file", + filename, fd); + close(fd); errno = EINVAL; return -1; } - if (cap_d == NULL) { - _cap_debug("removing filename capabilities"); - return removexattr(filename, XATTR_NAME_CAPS); + /* + * While the fd remains open, this named file is locked to the + * origin regular file. The size of the fdpath variable is + * sufficient to support a 160+ bit number. + */ + if (snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", fd) + >= sizeof(fdpath)) { + _cap_debug("file descriptor too large %d", fd); + errno = EINVAL; + ret = -1; + + } else if (cap_d == NULL) { + _cap_debug("dropping file caps on [%s] via [%s]", + filename, fdpath); + ret = removexattr(fdpath, XATTR_NAME_CAPS); + } else if (_fcaps_save(&rawvfscap, cap_d, &sizeofcaps) != 0) { - return -1; - } + _cap_debug("problem converting cap_d to vfscap format"); + ret = -1; - _cap_debug("setting filename capabilities"); - return setxattr(filename, XATTR_NAME_CAPS, &rawvfscap, sizeofcaps, 0); + } else { + _cap_debug("setting filename capabilities"); + ret = setxattr(fdpath, XATTR_NAME_CAPS, &rawvfscap, + sizeofcaps, 0); + } + close(fd); + return ret; } /* diff --git a/progs/quicktest.sh b/progs/quicktest.sh index 6aa2598..334e9f6 100755 --- a/progs/quicktest.sh +++ b/progs/quicktest.sh @@ -132,7 +132,19 @@ pass_capsh --secbits=0x2f --print -- -c "./privileged --uid=$nouid" fail_capsh --drop=cap_setuid --secbits=0x2f --print -- -c "./privileged --uid=$nouid" # change the way the capability is obtained (make it inheritable) +chmod 0000 ./privileged ./setcap cap_setuid,cap_setgid=ei ./privileged +if [ $? -ne 0 ]; then + echo "FAILED to set file capability" + exit 1 +fi +chmod 0755 ./privileged +ln -s privileged unprivileged +./setcap -r ./unprivileged +if [ $? -eq 0 ]; then + echo "FAILED by removing a capability from a symlinked file" + exit 1 +fi # Note, the bounding set (edited with --drop) only limits p # capabilities, not i's. @@ -216,7 +228,7 @@ EOF pass_capsh --iab='!%cap_chown,^cap_setpcap,cap_sys_admin' fail_capsh --mode=PURE1E --iab='!%cap_chown,^cap_sys_admin' fi -/bin/rm -f ./privileged +/bin/rm -f ./privileged ./unprivileged echo "testing namespaced file caps" -- 2.53.0