TLDR: A distro-specific Linux LPE found by harnessing LLMs into better multihop knowledge composition. Read on for affected distros, mitigations, and vulnerability details.
Background #
In Getting LLMs Drunk to Find Remote Linux Kernel OOB Writes (and More), I’d mentioned how improving LLMs’ ability to compose existing knowledge is a promising avenue for unlocking “creative” – or at least non-trivial – vulnerability findings. Incidentally, among the latest slew of Linux LPEs, CopyFail stood out for – among other things – exquisitely composing several logic bugs, serving as a reminder of the massive potential value of the approach. Unfortunately, training a capable looped transformer to improve compositionality was a non-starter, so I started looking for harness-level improvements instead.
GraphWalk: Enabling Reasoning in Large Language Models through Tool-Based Graph Navigation offered a promising alternative: the authors developed a tool for models to traverse (and reason through) graphs, improving their multihop reasoning capabilities. The benefits were measured primarily for non-reasoning models; but, on large-enough graphs, non-reasoning models equipped with the tool outperformed reasoning models without it. So, the general approach seemed promising even for otherwise-scaffolded reasoning models – slicing the context with RLMs, .md files with “memories,” etc., are all useful for tackling graph-based problems, but we could still strengthen the harness with a first-class graph traversal tool.
The paper described a tool for existing graphs, but for vulnerability hunting we don’t actually have “interesting” graphs pre-built (CodeQL-style CPGs are too low-level/clunky for the level of abstraction I wanted)! So I harnessed the agents to a) build the graphs at a higher level of abstraction and b) actually query them, like in the paper above. The graphs were intended to capture the following (deliberately somewhat fuzzy to play to LLMs’ strengths):
- Privileged consumers: what kernel paths, daemons, helpers, etc. consume an object as authoritative?
- Creators: what actions create or modify (in some way) the object? Are they attacker-controlled? Under what conditions?
- Object: what is the exact kernel object – e.g. key, policy verdict, fd, queue entry, signature, etc.?
- Check split: what security-relevant properties of the object are checked at creation time vs. later?
- Drift: (the most important) how can an object’s origin, credentials, namespace, idmap, mount view, LSM domain, etc. stop matching what’s assumed by its consumer?
At this point, I had the scaffolding to point at the desired target. Ideally, it’d be something straddling kernel and userspace for compositionality to really shine, and primarily/entirely a logic bug chain… Based on some prior experience, the SMB protocol family seemed like a fertile ground.
The vulnerability #
The harnessed agents found an issue at the intersection of kernel’s CIFS
and the userspace cifs-utils-provided helper.
In short, they first discovered that
the kernel did not validate the description origin of the cifs.spnego key object. Backtracking, they
then found that they could therefore issue a request_key() syscall with a fake key description, which
launches a rootful helper. Finally, after noticing that the fake key descriptions have actual security
relevance – they contain pid, which combined with upcall_target=app controls which namespace the
helper actually runs in – they converted the namespace confusion into root on the machine.
Since the patch has been out for over a week and is queued for stable, we agreed with linux-distros@ on an embargo through May 27, 2026. The advisory is now public so that the affected system owners can patch or apply other mitigations. The CVE assignment is still pending.
CIFS basics #
CIFS/SMB is a Windows-style network filesystem protocol. On Linux, the CIFS kernel
client handles the actual filesystem parts: mounting the share, talking SMB to the server,
doing reads/writes, etc. But, understandably, for Kerberos-auth’d mounts, kernel CIFS
doesn’t roll its own auth stack and instead relies on a userspace helper provided by cifs-utils.
The interaction happens through Linux keyrings. The kernel requests a cifs.spnego-type key,
and the normal keyutils/request-key config runs cifs.upcall as root to
fetch or build the Kerberos/SPNEGO material. That brings us to – ahem – the key part.
Bird’s eye view #
The expected interaction between the kernel and userspace parts is the following:
- Kernel CIFS decides it needs Kerberos/SPNEGO material for a mount.
- Kernel CIFS builds a semicolon-separated
cifs.spnegodescription string from real kernel state: server, uid, creduid, pid, namespace target, etc. For example:ver=0x2;host=fs.acme.com;ip4=192.168.1.10;sec=krb5;uid=0x3e8;creduid=0x3e8;user=test@ACME.COM;pid=0x4f2a;upcall_target=app - Kernel CIFS calls
request_key()for acifs.spnegokey while using its privatespnego_cred. - In userspace,
/sbin/request-keychecks the rules forcifs.spnego(e.g., the defaultcreate cifs.spnego * * /usr/sbin/cifs.upcall %kin/etc/request-key.d/cifs.spnego.conf) and callscifs.upcallas root. cifs.upcallthen parses the description and uses it to decide which uid, credential cache, process, and namespaces to use.- If the upcall succeeds, kernel CIFS gets back the SPNEGO blob and continues the mount/session setup.
You may have already noticed the critical question: does either userspace or the kernel validate that the key description fields actually came from kernel CIFS? That’s where things break:
- An attacker in userspace can call
request_key("cifs.spnego", totally_fake_description, ...)directly. - In the kernel, the pre-patch
cifs.spnegokey type does not reject the untrusted userspace-created descriptions, treating them as if they came from kernel CIFS. - Since the requested key type is
cifs.spnego,/sbin/request-keycallscifs.upcallas root per the defaultcifs.spnegorule, just like in the happy-path scenario above. - The userspace helper then parses attacker-controlled
pid,uid,creduid, andupcall_targetfields, assuming them to be kernel-produced. - With the attacker-fed
upcall_target=app, the helper switches into the namespaces of the suppliedpid. - Before the final
setuid()/setgid()/privilege drop, the helper does account lookup. Account lookup involves NSS, which permits loading of NSS modules based on the NSS config. - So, the attacker’s mount namespace can contain a fake
nsswitch.confand alibnss_*.so.2NSS module, getting the root helper to trigger the loading of attacker-controlled NSS code.
Diving in #
Now, let’s walk through what actually enables the PoC to convert the above to root:
Letting userspace speak as kernel CIFS #
A normal Kerberos CIFS mount eventually reaches cifs_get_spnego_key(). The kernel builds a cifs.spnego description string from kernel state, then asks the keyring subsystem for a cifs.spnego-type key under CIFS’s private spnego_cred:
dp += sprintf(dp, ";uid=0x%x", ...);
dp += sprintf(dp, ";creduid=0x%x", ...);
dp += sprintf(dp, ";pid=0x%x", current->pid);
...
if (sesInfo->upcall_target == UPTARGET_MOUNT)
dp += sprintf(dp, ";upcall_target=mount");
else
dp += sprintf(dp, ";upcall_target=app");
...
scoped_with_creds(spnego_cred)
spnego_key = request_key(&cifs_spnego_key_type, description, "");These fields actually matter: uid/creduid decide whose credentials the helper should look up, while pid/upcall_target determine what the helper should treat as the application’s namespace.
But the key type definition did not enforce that the key was kernel-CIFS-originating. Before the fix, cifs_spnego_key_type was just:
struct key_type cifs_spnego_key_type = {
.name = "cifs.spnego",
.instantiate = cifs_spnego_key_instantiate,
.destroy = cifs_spnego_key_destroy,
.describe = user_describe,
};(Note the missing .vet_description, the hook that would govern a given key_type’s description’s legitimacy). So, an unprivileged process could ask for the same cifs.spnego-type key with request_key("cifs.spnego", totally_fake_description, ...), and the default request-key rule (create cifs.spnego * * /usr/sbin/cifs.upcall %k) would still launch cifs.upcall as root.
Importantly, the kernel did not need to return a key for cifs.upcall to launch. The upcall launches first, enabling the attack, even if the kernel -ENOKEYs after.
Forged pid –> root NSS #
On the userspace side, cifs.upcall parses the key description and treats the decoded fields as kernel-provided facts regardless of where they came from. The namespace-aware code parses upcall_target=mount / upcall_target=app and then switches namespaces when the upcall target is app:
/*
* Change to the process's namespace. This means that things will work
* acceptably in containers, because we'll be looking at the correct
* filesystem and have the correct network configuration.
*/
if (arg->upcall_target == UPTARGET_APP ||
arg->upcall_target == UPTARGET_UNSPECIFIED) {
syslog(LOG_INFO,
"upcall_target=app, switching namespaces to application thread");
rc = switch_to_process_ns(arg->pid);
if (rc == -1)
goto out;
if (trim_capabilities(env_probe))
goto out;
}The comment explains why this is a supported mode of operation; it also reveals the risk of managing to thread through an attacker-controlled arg->pid.
Only after that namespace switch does it get the target gid out of the passwd NSS database with getpwuid(uid). The final identity transition happens later, when the helper reaches setuid(uid) and drop_all_capabilities():
/*
* The kernel doesn't pass down the gid, so we resort here to scraping
* one out of the passwd nss db.
*/
pw = getpwuid(uid);
...
rc = setgroups(0, NULL);
...
rc = setgid(pw->pw_gid);
...
env_cachename = get_cachename_from_process_env(...);
rc = setuid(uid);
...
rc = drop_all_capabilities();And to reiterate, getpwuid(0) goes through NSS. If the process has already switched into an attacker-controlled mount namespace, NSS can mean the following is executed as root:
/etc/nsswitch.conf -> passwd: pwn files
libnss_pwn.so.2 -> loaded by the root helperAt this point, libnss_pwn.so.2 can drop a sudoers.d config with the attacker’s username, as in the PoC.
The fix #
The bare minimum kernel-side fix is to treat the descriptions as legitimate only when CIFS is using spnego_cred:
static int cifs_spnego_key_vet_description(const char *description)
{
if (current_cred() != spnego_cred)
return -EPERM;
return 0;
}
struct key_type cifs_spnego_key_type = {
.name = "cifs.spnego",
.vet_description = cifs_spnego_key_vet_description,
...
};There’s still userspace hardening to be done to not assume that the key description is necessarily kernel-generated, but the above stops the exploitation by itself.
Are you affected? + Mitigation #
The exploitability conditions are all of the below:
- Vulnerable kernel version. The kernel-side bug has been around since 2007
- An affected
cifs-utilsversion (and the defaultcifs.spnegorule it comes with). Nominally, this is 6.14 and higher, but backports of other CVE fixes have introduced issues into oldercifs-utilsas well (see the Distro impact tables below) - Unprivileged users must be able to create user (and mount) namespaces
- SELinux/AppArmor/etc. policies that do not get in the way (the defaults vary by distro/version, see the Distro impact tables below)
Aside from applying the backported kernel patch, you can mitigate via any of the following:
- Blocking the
cifsmodule from loading (if not required), assuming it’s not built-in - Removing
cifs-utils(if not required) - Deleting/overriding the default cifs.spnego request-key rule (if Kerberos auth is not
required), e.g. (adjusting for your
keyctlpath):
cat >/etc/request-key.d/cifs.spnego.conf <<'EOF'
create cifs.spnego * * /usr/sbin/keyctl negate %k 30 %S
EOF- Disabling unprivileged user namespaces
You can use the released PoC to validate the mitigations.
Distro impact tables #
A very non-exhaustive list of systems tested.
Stock-exploitable #
Here, cifs-utils is installed by default and the default distro config (LSMs/etc.) does not stop the
exploitation:
| Target | Details |
|---|---|
| Linux Mint 21.3/22.3 Cinnamon | Exploitable with AppArmor active |
| CentOS Stream 9 GNOME | Exploitable with SELinux enforcing |
| Rocky Linux 9 Workstation | Exploitable with SELinux enforcing |
| Kali Linux 2021.4/2022.4/2023.4/2024.4/2025.4/2026.1 headless | Exploitable with AppArmor active |
| AlmaLinux 9.7 Workstation/Azure cloud image | Exploitable with SELinux enforcing |
| SLES 15 SP7/SAP 15 SP7 | Exploitable with AppArmor active |
| SLES SAP 16 | Exploitable with SELinux permissive |
Stock-policy exploitable if cifs-utils is installed #
Exploitable under default distro config, but cifs-utils needs to be installed manually:
| Target | Details |
|---|---|
| Ubuntu 18.04/20.04/22.04 Desktop/Server | Exploitable with AppArmor active |
| Pop!_OS 22.04 Intel/24.04 Generic | Exploitable with AppArmor active |
| Ubuntu 24.04 Desktop minimal/full and Server | Direct unshare is blocked by AppArmor userns policy; exploitable through aa-exec -p trinity ‘trick’ |
| Debian 11/12/13 netinst standard and GNOME/KDE/standard/XFCE | Exploitable with AppArmor active |
| CentOS Stream 9 Cinnamon/KDE/MATE/XFCE | Exploitable with SELinux enforcing |
| Rocky Linux 9 KDE/Workstation-Lite | Exploitable with SELinux enforcing |
| openSUSE Leap 15.6 GNOME/KDE | Exploitable with AppArmor active |
| Rocky Linux 8 GenericCloud | Exploitable with SELinux enforcing |
| Oracle Linux 8/9 KVM | Exploitable with SELinux enforcing |
| Amazon Linux 2023 KVM | Exploitable with SELinux permissive |
Blocked by stock policy #
The default distro config (LSMs/etc.) blocks exploitation even if cifs-utils is
present:
| Target | cifs-utils installed by default? | Details |
|---|---|---|
| Ubuntu 26.04 Desktop minimal/full and Server | no | Blocked by AppArmor userns policy by default; exploitable after AppArmor userns sysctls are relaxed |
| Fedora 40/41/42/43/44 Workstation/Server | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| CentOS Stream 10 GNOME | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| CentOS Stream 10 KDE | no | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| Rocky Linux 10 Workstation | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| Rocky Linux 10 KDE/Workstation-Lite | no | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| AlmaLinux 10.1 Workstation/Azure cloud image recipe | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| Oracle Linux 10 KVM | no | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| openSUSE Tumbleweed GNOME/KDE | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| openSUSE Leap 16.0 OEM GNOME/KDE | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| openSUSE Leap 16.0 Minimal-VM | no | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
| SLES 16 | yes | Blocked by SELinux enforcing by default; exploitable after setenforce 0 |
Unaffected #
The two tested cases where cifs-utils is too old to be exploitable:
| Target | cifs-utils installed by default? | Details |
|---|---|---|
| Amazon Linux 2 KVM | no | Unaffected by this PoC: cifs-utils 6.2 lacks the namespace-switch path |
| Kali Linux 2019.4/2020.4 | yes | Unaffected by this PoC after userns relaxation: cifs-utils 6.9 lacks the namespace-switch path |
Conclusion #
Ultimately, I was curious if the models could build non-trivial, multihop chains given the right tools. By producing and walking a semantic graph of security-relevant objects and properties – not so in the weeds that they got stifled by the low-level definitions, but not so abstract that they flailed aimlessly – the models arrived at:
forged userspace cifs.spnego request
-> root cifs.upcall is launched by the normal request-key rule
-> cifs.upcall trusts fake, not-actually-kernel-originating fields
-> fake pid + upcall_target=app moves the root helper into an attacker-controlled namespace
-> NSS lookup happens before the final privilege drop
-> namespace-local NSS module is loaded inside the root helper, writing to sudoers.dWhile the primitives themselves are not groundbreaking, the chain is pretty neat, and is much more exciting than the earlier “drunk” ksmbd memory safety findings! The graph-based approach was likely not strictly necessary, as simple Markdown memory may have sufficed, but it did seem to enable the agents to systematically burn down the potential exploit lanes with ease.