§ CM

CIFSwitch: a 19-year-old local root that only fires if you let it

A researcher going by Asim Manizada published CIFSwitch on May 28, a local privilege escalation in the Linux kernel’s CIFS client that hands root to any unprivileged user on the box. The bug has been sitting in the tree since 2007, which makes it about nineteen years old, and it was found with an AI-assisted analysis pass that walked a semantic graph of privileged callers and the checks that guard them. No CVE is assigned yet; the request is in but unallocated as of this writing.

The headline going around (“gives root on multiple distributions”) is true and also oversold. CIFSwitch is real and the kernel flaw is genuinely a flaw, but your exposure is gated by three things that have nothing to do with the kernel version you happen to be running. If you patch nothing and read nothing else, the one-line version is: it bites the boxes that are running cifs-utils they don’t need, with unprivileged user namespaces left on, and no LSM in the way. That is a hygiene profile, not a kernel CVE.

The bug itself

The kernel registers a key type called cifs.spnego. When you mount an SMB share with Kerberos, the in-kernel CIFS client needs a SPNEGO blob to authenticate, and it can’t do Kerberos itself, so it kicks the request up to userspace through the request-key mechanism. The userspace side is cifs.upcall, shipped in cifs-utils, and it runs as root because fetching credentials is a privileged operation.

The defect is small and entirely a missing check. In fs/smb/client/cifs_spnego.c, the cifs_spnego_key_type struct was registered without a .vet_description hook:

struct key_type cifs_spnego_key_type = {
    .name        = "cifs.spnego",
    .instantiate = cifs_spnego_key_instantiate,
    .destroy     = cifs_spnego_key_destroy,
    .describe    = user_describe,
};

.vet_description is the hook the keyrings subsystem calls to let a key type reject a description it doesn’t like. Without it, the kernel never asks the obvious question: did this cifs.spnego request actually come from the kernel’s own CIFS client, or did some random user process call request_key("cifs.spnego", ...) directly with a description it made up? The answer, for nineteen years, was that it didn’t check. So an unprivileged process can forge the request and the kernel will dutifully spawn the root-owned upcall handler to service it.

How it’s executed

The “switch” in the name is the good part. Walk the chain:

A local unprivileged attacker calls request_key("cifs.spnego", <forged description>, ...). The kernel sees a request for a key type it knows, finds no matching key, and spawns /sbin/request-key, which consults its rules and runs cifs.upcall as root. So far the only thing wrong is that the request was allowed to originate from userspace at all.

Now the second half. cifs.upcall parses the description and trusts fields that came straight from the attacker: pid, uid, creduid, and upcall_target. When upcall_target=app, the handler switches into the namespaces of the supplied pid so it can resolve credentials in the caller’s context. That is switch_to_process_ns(arg->pid), and the pid belongs to the attacker, so root has now stepped into a mount namespace the attacker fully controls.

Then the handler does a getpwuid(uid) to look up the user, and that triggers an NSS lookup, and the NSS lookup happens before the process drops privileges with setuid. In the attacker’s mount namespace, /etc/nsswitch.conf and the libnss_*.so.2 modules are whatever the attacker put there. The kernel loads the attacker’s shared object and runs its code as root. From there it is a sudoers.d drop or whatever else you want. The public PoC does it in a single command.

So the flaw is two trust failures stacked: the kernel trusting that a cifs.spnego request is internal, and cifs.upcall trusting attacker-supplied namespace and identity fields before it drops privilege. Either check alone would have broken the chain.

What’s actually impacted

This is where the “multiple distributions” framing needs the asterisk. Three preconditions have to all hold:

  • cifs-utils is installed, because the whole chain runs through cifs.upcall. No cifs.upcall, no root.
  • Unprivileged user and mount namespaces are allowed, because the attacker needs to build the poisoned namespace.
  • No LSM is sitting on the path. SELinux in enforcing mode blocks the public exploit; AppArmor profiles can too.

Line those up against real distros and you get a split that has very little to do with how new your kernel is.

Posture Distributions (stock)
Exploitable out of the box Linux Mint 21.3 / 22.3, CentOS Stream 9, Rocky Linux 9, AlmaLinux 9, SLES 15 SP7, headless Kali 2021.4+
Exploitable if you install cifs-utils Ubuntu 18.04–24.04, Debian 11–13, openSUSE Leap 15.6
Blocked by default LSM policy Ubuntu 26.04, Fedora 40+, CentOS Stream 10+, Rocky 10+, AlmaLinux 10.1+

The pattern is not subtle. The RHEL-9 generation lights up because those builds ship without SELinux confining this path and frequently carry cifs-utils; the RHEL-10 generation is fine in the same scenario because the policy got tightened. Mint and headless Kali get caught because they are permissive desktops and pentest boxes that ship a lot and confine little.

Here is the opinion I will plant a flag on: the interesting story in CIFSwitch is not the kernel bug, it is how many production boxes are carrying cifs-utils for no reason anyone can name. The package gets pulled in by a dependency or installed during a one-time troubleshooting session and never removed. The kernel flaw is the match; the unused package is the gas. Patch the kernel, certainly, but the boxes that were exploitable were already failing least-functionality (NIST CM-7) before this PoC existed, and they will fail the next one too.

Detecting it

Be honest with yourself about detection here: it is weak, and this is a patch-and-reduce-surface problem far more than a hunt-for-it problem. Almost nobody logs request_key syscalls, so the forged upcall itself is invisible on a default build. What you can actually catch lives one layer up, in EDR or auditd:

  • cifs.upcall executing when there is no corresponding SMB mount activity. A credential helper firing with nothing mounting is the tell.
  • A root process entering another process’s namespace and then dlopen-ing a libnss_*.so from a path that isn’t the system library directory. If your EDR surfaces setns plus an unusual shared-object load, that is the chain.
  • An auditd watch on writes to /etc/sudoers.d/ and /etc/cron.d/ will catch the payload, though by then you are detecting the result, not the exploit.

Expect the first two to be noisy until you carve out legitimate Kerberized SMB usage, and on a host that genuinely mounts SMB shares with sec=krb5 you may not be able to cleanly separate the signal at all. That is the honest limitation: the detection that works best is the one you get for free by not having cifs.upcall on the box.

Mitigation, by what you actually run

The real fix is the kernel patch, upstream commit 3da1fdf4efbc (“smb: client: reject userspace cifs.spnego descriptions”), which adds the missing .vet_description hook so the key type enforces current_cred() == spnego_cred and rejects anything that didn’t originate under the CIFS client’s own credentials. Distro kernels are rolling: AlmaLinux has 5.14.0-687.5.4.el9_8 for 9, 4.18.0-553.126.2.el8_10 for 8, and 6.12.0-211.7.4.el10_2 for 10 in testing. Get on a patched kernel and the rest of this section is belt-and-suspenders. But patched kernels need a reboot, and reboots need a window, so here is the interim menu ordered by how I would actually reach for them:

If you don’t mount SMB shares, remove the package. dnf remove cifs-utils or apt purge cifs-utils, and blacklist the module for good measure (echo 'install cifs /bin/true' > /etc/modprobe.d/cifs-disable.conf). This is the cleanest mitigation because it also fixes the underlying hygiene problem, and on a server that has no business talking SMB it costs you nothing.

If you need cifs-utils but not the upcall, neutralize the handler. Drop a rule in /etc/request-key.d/cifs.spnego.conf pointing the upcall at /bin/false. This kills the chain and also kills Kerberized SMB mounts, so only do it where you mount with NTLM or not at all.

Disable unprivileged user namespaces, with a caveat. On RHEL-family, sysctl -w user.max_user_namespaces=0; on Debian/Ubuntu, sysctl -w kernel.unprivileged_userns_clone=0. This breaks the attacker’s namespace setup, but it also breaks rootless Podman, the Chrome/Electron sandbox, and a pile of CI tooling. On a hardened server, fine. On a developer workstation, you will get tickets within the hour, so weigh it.

Let SELinux do its job, but don’t lean on it alone. Enforcing mode blocks the published PoC, which is a genuine reason RHEL-10 and friends shrug this off. It is good defense in depth and a bad sole control: the kernel flaw is still present, and a tailored exploit may route around the specific policy denial the public PoC trips.

If you run a STIG’d fleet, go check before you change anything. The RHEL-9 STIG already calls for user.max_user_namespaces set to 0 on most profiles, and least-functionality requirements should have had cifs-utils off the build to begin with. A correctly STIG’d host may already be covered on two of the three preconditions. “Should be” is doing work in that sentence, which is exactly why you verify with a scan rather than assume. Your ACAS / Nessus run will flag the unpatched kernel once the plugin lands, and that is the artifact your AO will want anyway under SI-2 flaw remediation and RA-5 vulnerability monitoring.

The part worth keeping

A logic bug hid in the kernel’s SMB client for nineteen years, through countless audits, and an AI-assisted pass that models privileged callers and missing checks walked straight to it. That is the genuinely new thing here, and it is going to keep happening to old, gnarly, trusted code that nobody re-reads. The defense did not change, though. Patch the kernel, then go count how many of your hosts are carrying cifs-utils for a reason nobody can explain, and take it off the ones that can’t answer. The flaw is the kernel’s. The attack surface was yours.

Sources