Skip to main content

CIFSwitch: a non-universal Linux local root vulnerability

·2326 words·11 mins
Site logo
Author
Asim Viladi Oglu Manizada

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:

  1. Kernel CIFS decides it needs Kerberos/SPNEGO material for a mount.
  2. 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
  3. Kernel CIFS calls request_key() for a cifs.spnego key while using its private spnego_cred.
  4. 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.
  5. cifs.upcall then parses the description and uses it to decide which uid, credential cache, process, and namespaces to use.
  6. 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:

  1. An attacker in userspace can call request_key("cifs.spnego", totally_fake_description, ...) directly.
  2. In the kernel, the pre-patch cifs.spnego key type does not reject the untrusted userspace-created descriptions, treating them as if they came from kernel CIFS.
  3. 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.
  4. The userspace helper then parses attacker-controlled pid, uid, creduid, and upcall_target fields, assuming them to be kernel-produced.
  5. With the attacker-fed upcall_target=app, the helper switches into the namespaces of the supplied pid.
  6. 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.
  7. 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
#

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 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 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-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 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 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 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 trinitytrick
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.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 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.