# CIFSwitch: A non-universal Linux local root vulnerability

> Source: <https://heyitsas.im/posts/cifswitch/>
> Published: 2026-05-28 18:18:31+00:00

*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
[#](#background)

In [Getting LLMs Drunk to Find Remote Linux Kernel OOB
Writes (and More)](https://heyitsas.im/posts/drinking-llms/#future-research-directions),
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](https://arxiv.org/html/2604.01610v1) 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](https://nvd.nist.gov/vuln/detail/CVE-2026-31432)
[prior](https://nvd.nist.gov/vuln/detail/CVE-2026-31433)
[experience](https://www.samba.org/samba/security/CVE-2026-1933.html), the SMB protocol
family seemed like a fertile ground.

## The vulnerability
[#](#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](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2) 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](/posts/cifswitch/#are-you-affected--mitigation). The CVE assignment is still pending.

### CIFS basics
[#](#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
[#](#birds-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.spnego`

description 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 a`cifs.spnego`

key while using its private`spnego_cred`

. *In userspace*,`/sbin/request-key`

checks the rules for`cifs.spnego`

(e.g., the default`create cifs.spnego * * /usr/sbin/cifs.upcall %k`

in`/etc/request-key.d/cifs.spnego.conf`

) and calls`cifs.upcall`

as root.`cifs.upcall`

then 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](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2)`cifs.spnego`

key 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-key`

calls`cifs.upcall`

as root per the default`cifs.spnego`

rule, just like in the happy-path scenario above. - The userspace helper then parses attacker-controlled
`pid`

,`uid`

,`creduid`

, and`upcall_target`

fields, assuming them to be kernel-produced. - With the attacker-fed
`upcall_target=app`

, the helper switches into the namespaces of the supplied`pid`

. - Before the final
`setuid()`

/`setgid()`

/privilege drop, the helper does account lookup. Account lookup involves[NSS](https://en.wikipedia.org/wiki/Name_Service_Switch), which permits loading of NSS modules based on the NSS config. - So, the attacker’s mount namespace can contain a fake
`nsswitch.conf`

and a`libnss_*.so.2`

NSS module, getting the root helper to trigger the loading of attacker-controlled NSS code.

### Diving in
[#](#diving-in)

Now, let’s walk through what actually enables the [PoC](https://github.com/manizada/CIFSwitch) to convert the above to root:

#### Letting userspace speak as kernel CIFS
[#](#letting-userspace-speak-as-kernel-cifs)

A normal Kerberos CIFS mount eventually reaches [ cifs_get_spnego_key()](https://github.com/torvalds/linux/blob/4d8690dace005a38e6dbde9ecce2da3ad85c7c41/fs/smb/client/cifs_spnego.c#L83-L162). 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](https://github.com/torvalds/linux/blob/4d8690dace005a38e6dbde9ecce2da3ad85c7c41/fs/smb/client/cifs_spnego.c#L47-L52) 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 `-ENOKEY`

s after.

#### Forged pid –> root NSS
[#](#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](https://github.com/piastry/cifs-utils/blob/89b679228cc1be9739d54203d28289b03352c174/cifs.upcall.c#L1511-L1525) 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)](https://github.com/piastry/cifs-utils/blob/89b679228cc1be9739d54203d28289b03352c174/cifs.upcall.c#L1518-L1540). The final identity transition happens later, when the helper reaches

[:](https://github.com/piastry/cifs-utils/blob/89b679228cc1be9739d54203d28289b03352c174/cifs.upcall.c#L1568-L1579)

`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:

``` php
/etc/nsswitch.conf  ->  passwd: pwn files
libnss_pwn.so.2     ->  loaded by the root helper
```

At this point, `libnss_pwn.so.2`

can drop a `sudoers.d`

config with the attacker’s username, as in the PoC.

### The fix
[#](#the-fix)

The bare minimum kernel-side [fix](https://github.com/torvalds/linux/blob/3da1fdf4efbc490041eb4f836bf596201203f8f2/fs/smb/client/cifs_spnego.c#L44-L64) is to treat the descriptions as legitimate only when CIFS is using `spnego_cred`

:

``` js
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
[#](#are-you-affected--mitigation)

The exploitability conditions are all of the below:

- Vulnerable kernel version. The kernel-side bug has been around
[since 2007](https://github.com/torvalds/linux/blame/7ad785927d9eb348adb381d168ed73d0dd3c7670/fs/smb/client/cifs_spnego.c) - An affected
`cifs-utils`

version (and the default`cifs.spnego`

rule it comes with). Nominally, this is 6.14 and higher, but backports of other CVE fixes have introduced issues into older`cifs-utils`

as well (see thebelow)[Distro impact tables](/posts/cifswitch/#distro-impact-tables) - 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
below)[Distro impact tables](/posts/cifswitch/#distro-impact-tables)

Aside from applying the backported [kernel
patch](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2),
you can mitigate via any of the following:

- Blocking the
`cifs`

module 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
`keyctl`

path):

```
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](https://github.com/manizada/CIFSwitch) to validate the mitigations.

### Distro impact tables
[#](#distro-impact-tables)

A very non-exhaustive list of systems tested.

#### Stock-exploitable
[#](#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
[#](#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` ‘
|
| 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
[#](#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
[#](#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
[#](#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:

``` php
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.d
```

While the primitives themselves are not groundbreaking, the chain is pretty neat, and is much more exciting than the earlier
“drunk” [ksmbd memory safety](/posts/drinking-llms/) 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.
