{"slug": "cifswitch-a-non-universal-linux-local-root-vulnerability", "title": "CIFSwitch: A non-universal Linux local root vulnerability", "summary": "Security researchers discovered a Linux local privilege escalation vulnerability, dubbed CIFSwitch, in the kernel's CIFS filesystem and the userspace cifs-utils helper. The flaw allows an attacker to exploit a missing validation of key description origins, enabling a namespace confusion attack that grants root access on the machine. The vulnerability affects specific Linux distributions and has been patched in the upstream kernel.", "body_md": "*TLDR: A distro-specific Linux LPE found by harnessing LLMs into better multihop knowledge composition. Read on for affected distros, mitigations, and vulnerability details.*\n\n## Background\n[#](#background)\n\nIn [Getting LLMs Drunk to Find Remote Linux Kernel OOB\nWrites (and More)](https://heyitsas.im/posts/drinking-llms/#future-research-directions),\nI’d mentioned how improving LLMs’ ability to compose existing knowledge is a promising avenue for unlocking\n“creative” – or at least non-trivial – vulnerability findings. Incidentally, among the latest slew of Linux LPEs,\n**CopyFail** stood out for – among other things – exquisitely composing several logic bugs,\nserving as a reminder of the massive potential value of the approach. Unfortunately, training a capable looped\ntransformer to improve compositionality was a non-starter, so I started looking for harness-level improvements instead.\n\n[GraphWalk: Enabling\nReasoning in Large Language Models through Tool-Based Graph\nNavigation](https://arxiv.org/html/2604.01610v1) offered a promising alternative: the authors\ndeveloped a tool for models to traverse (and reason through) graphs, improving their multihop reasoning capabilities.\nThe benefits were measured primarily for non-reasoning models; but, on large-enough graphs, non-reasoning models equipped with the tool outperformed\nreasoning 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\nproblems, but we could still strengthen the harness with a first-class graph traversal tool.\n\nThe paper described a tool for *existing* graphs, but for vulnerability hunting we don’t actually have\n“interesting” graphs pre-built (CodeQL-style CPGs are too low-level/clunky for the level of abstraction I wanted)!\nSo I harnessed the agents to a) build the graphs at a higher level of abstraction *and* b) actually query them,\nlike in the paper above.\nThe graphs were intended to capture the following (deliberately somewhat fuzzy to play to LLMs’ strengths):\n\n- Privileged consumers: what kernel paths, daemons, helpers, etc. consume an object as authoritative?\n- Creators: what actions create or modify (in\n*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.?\n- Check split: what security-relevant properties of the object are checked at creation time vs. later?\n- 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?\n\nAt this point, I had the scaffolding to point at the desired target. Ideally, it’d be something\nstraddling kernel and userspace for compositionality to really shine, and primarily/entirely a logic bug chain…\nBased on [some](https://nvd.nist.gov/vuln/detail/CVE-2026-31432)\n[prior](https://nvd.nist.gov/vuln/detail/CVE-2026-31433)\n[experience](https://www.samba.org/samba/security/CVE-2026-1933.html), the SMB protocol\nfamily seemed like a fertile ground.\n\n## The vulnerability\n[#](#the-vulnerability)\n\nThe harnessed agents found an issue at the intersection of kernel’s CIFS\nand the userspace `cifs-utils`\n\n-provided helper.\n\nIn short, they first discovered that\nthe kernel did not validate the description origin of the `cifs.spnego`\n\nkey object. Backtracking, they\nthen found that they could therefore issue a `request_key()`\n\nsyscall with a fake key description, which\nlaunches a rootful helper. Finally, after noticing that the fake key descriptions have actual security\nrelevance – they contain `pid`\n\n, which combined with `upcall_target=app`\n\ncontrols *which namespace the\nhelper actually runs in* – they converted the namespace confusion into root on the machine.\n\nSince [the patch](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2) has been out for over a week\nand is queued for stable, we agreed with linux-distros@ on an embargo through May 27, 2026. The advisory is now public so that\nthe affected system owners can patch or apply [other mitigations](/posts/cifswitch/#are-you-affected--mitigation). The CVE assignment is still pending.\n\n### CIFS basics\n[#](#cifs-basics)\n\nCIFS/SMB is a Windows-style network filesystem protocol. On Linux, the CIFS kernel\nclient handles the actual filesystem parts: mounting the share, talking SMB to the server,\ndoing reads/writes, etc. But, understandably, for Kerberos-auth’d mounts, kernel CIFS\ndoesn’t roll its own auth stack and instead relies on a userspace helper provided by `cifs-utils`\n\n.\n\nThe interaction happens through Linux keyrings. The kernel requests a `cifs.spnego`\n\n-type key,\nand the normal keyutils/request-key config runs `cifs.upcall`\n\nas root to\nfetch or build the Kerberos/SPNEGO material. That brings us to – ahem – the *key* part.\n\n### Bird’s eye view\n[#](#birds-eye-view)\n\nThe *expected* interaction between the kernel and userspace parts is the following:\n\n- Kernel CIFS decides it needs Kerberos/SPNEGO material for a mount.\n- Kernel CIFS builds a semicolon-separated\n`cifs.spnego`\n\ndescription 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`\n\n- Kernel CIFS calls\n`request_key()`\n\nfor a`cifs.spnego`\n\nkey while using its private`spnego_cred`\n\n. *In userspace*,`/sbin/request-key`\n\nchecks the rules for`cifs.spnego`\n\n(e.g., the default`create cifs.spnego * * /usr/sbin/cifs.upcall %k`\n\nin`/etc/request-key.d/cifs.spnego.conf`\n\n) and calls`cifs.upcall`\n\nas root.`cifs.upcall`\n\nthen 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.\n\nYou 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:\n\n- An attacker in userspace can call\n`request_key(\"cifs.spnego\", totally_fake_description, ...)`\n\ndirectly. - In the kernel, the pre-\n[patch](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2)`cifs.spnego`\n\nkey type does not reject the untrusted userspace-created descriptions, treating them as if they came from kernel CIFS. - Since the requested key type is\n`cifs.spnego`\n\n,`/sbin/request-key`\n\ncalls`cifs.upcall`\n\nas root per the default`cifs.spnego`\n\nrule, just like in the happy-path scenario above. - The userspace helper then parses attacker-controlled\n`pid`\n\n,`uid`\n\n,`creduid`\n\n, and`upcall_target`\n\nfields, assuming them to be kernel-produced. - With the attacker-fed\n`upcall_target=app`\n\n, the helper switches into the namespaces of the supplied`pid`\n\n. - Before the final\n`setuid()`\n\n/`setgid()`\n\n/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\n`nsswitch.conf`\n\nand a`libnss_*.so.2`\n\nNSS module, getting the root helper to trigger the loading of attacker-controlled NSS code.\n\n### Diving in\n[#](#diving-in)\n\nNow, let’s walk through what actually enables the [PoC](https://github.com/manizada/CIFSwitch) to convert the above to root:\n\n#### Letting userspace speak as kernel CIFS\n[#](#letting-userspace-speak-as-kernel-cifs)\n\nA 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\n\n`cifs.spnego`\n\ndescription string from kernel state, then asks the keyring subsystem for a `cifs.spnego`\n\n-type key under CIFS’s private `spnego_cred`\n\n:\n\n```\ndp += sprintf(dp, \";uid=0x%x\", ...);\ndp += sprintf(dp, \";creduid=0x%x\", ...);\ndp += sprintf(dp, \";pid=0x%x\", current->pid);\n\n...\n\nif (sesInfo->upcall_target == UPTARGET_MOUNT)\n    dp += sprintf(dp, \";upcall_target=mount\");\nelse\n    dp += sprintf(dp, \";upcall_target=app\");\n\n...\n\nscoped_with_creds(spnego_cred)\n    spnego_key = request_key(&cifs_spnego_key_type, description, \"\");\n```\n\nThese fields actually matter: `uid`\n\n/`creduid`\n\ndecide whose credentials the helper should look up, while `pid`\n\n/`upcall_target`\n\ndetermine what the helper should treat as the application’s namespace.\n\nBut 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:\n\n```\nstruct key_type cifs_spnego_key_type = {\n    .name        = \"cifs.spnego\",\n    .instantiate = cifs_spnego_key_instantiate,\n    .destroy     = cifs_spnego_key_destroy,\n    .describe    = user_describe,\n};\n```\n\n(Note the missing `.vet_description`\n\n, the hook that would govern a given `key_type`\n\n’s description’s legitimacy). So, an unprivileged process could ask for the same `cifs.spnego`\n\n-type key with `request_key(\"cifs.spnego\", totally_fake_description, ...)`\n\n, and the default request-key rule (`create cifs.spnego * * /usr/sbin/cifs.upcall %k`\n\n) would still launch `cifs.upcall`\n\nas root.\n\nImportantly, *the kernel did not need to return a key* for `cifs.upcall`\n\nto launch. The upcall launches first, enabling the attack, even if the kernel `-ENOKEY`\n\ns after.\n\n#### Forged pid –> root NSS\n[#](#forged-pid--root-nss)\n\nOn the userspace side, `cifs.upcall`\n\nparses 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`\n\n/ `upcall_target=app`\n\nand then [switches namespaces](https://github.com/piastry/cifs-utils/blob/89b679228cc1be9739d54203d28289b03352c174/cifs.upcall.c#L1511-L1525) when the upcall target is `app`\n\n:\n\n```\n/*\n * Change to the process's namespace. This means that things will work\n * acceptably in containers, because we'll be looking at the correct\n * filesystem and have the correct network configuration.\n */\nif (arg->upcall_target == UPTARGET_APP ||\n    arg->upcall_target == UPTARGET_UNSPECIFIED) {\n    syslog(LOG_INFO,\n           \"upcall_target=app, switching namespaces to application thread\");\n\n    rc = switch_to_process_ns(arg->pid);\n    if (rc == -1)\n        goto out;\n\n    if (trim_capabilities(env_probe))\n        goto out;\n}\n```\n\nThe 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`\n\n.\n\nOnly *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\n\n[:](https://github.com/piastry/cifs-utils/blob/89b679228cc1be9739d54203d28289b03352c174/cifs.upcall.c#L1568-L1579)\n\n`setuid(uid)`\n\nand `drop_all_capabilities()`\n\n```\n/*\n * The kernel doesn't pass down the gid, so we resort here to scraping\n * one out of the passwd nss db.\n */\npw = getpwuid(uid);\n...\nrc = setgroups(0, NULL);\n...\nrc = setgid(pw->pw_gid);\n...\nenv_cachename = get_cachename_from_process_env(...);\n\nrc = setuid(uid);\n...\nrc = drop_all_capabilities();\n```\n\nAnd to reiterate, `getpwuid(0)`\n\ngoes through NSS. If the process has already switched into an attacker-controlled mount namespace, NSS can mean the following is executed as root:\n\n``` php\n/etc/nsswitch.conf  ->  passwd: pwn files\nlibnss_pwn.so.2     ->  loaded by the root helper\n```\n\nAt this point, `libnss_pwn.so.2`\n\ncan drop a `sudoers.d`\n\nconfig with the attacker’s username, as in the PoC.\n\n### The fix\n[#](#the-fix)\n\nThe 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`\n\n:\n\n``` js\nstatic int cifs_spnego_key_vet_description(const char *description)\n{\n    if (current_cred() != spnego_cred)\n        return -EPERM;\n\n    return 0;\n}\n\nstruct key_type cifs_spnego_key_type = {\n    .name = \"cifs.spnego\",\n    .vet_description = cifs_spnego_key_vet_description,\n    ...\n};\n```\n\nThere’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.\n\n## Are you affected? + Mitigation\n[#](#are-you-affected--mitigation)\n\nThe exploitability conditions are all of the below:\n\n- Vulnerable kernel version. The kernel-side bug has been around\n[since 2007](https://github.com/torvalds/linux/blame/7ad785927d9eb348adb381d168ed73d0dd3c7670/fs/smb/client/cifs_spnego.c) - An affected\n`cifs-utils`\n\nversion (and the default`cifs.spnego`\n\nrule it comes with). Nominally, this is 6.14 and higher, but backports of other CVE fixes have introduced issues into older`cifs-utils`\n\nas well (see thebelow)[Distro impact tables](/posts/cifswitch/#distro-impact-tables) - Unprivileged users must be able to create user (and mount) namespaces\n- SELinux/AppArmor/etc. policies that do not get in the way (the defaults vary by\ndistro/version, see the\nbelow)[Distro impact tables](/posts/cifswitch/#distro-impact-tables)\n\nAside from applying the backported [kernel\npatch](https://github.com/torvalds/linux/commit/3da1fdf4efbc490041eb4f836bf596201203f8f2),\nyou can mitigate via any of the following:\n\n- Blocking the\n`cifs`\n\nmodule from loading (if not required), assuming it’s not built-in - Removing\n`cifs-utils`\n\n(if not required) - Deleting/overriding the default cifs.spnego request-key rule (if Kerberos auth is not\nrequired), e.g. (adjusting for your\n`keyctl`\n\npath):\n\n```\ncat >/etc/request-key.d/cifs.spnego.conf <<'EOF'\ncreate cifs.spnego * * /usr/sbin/keyctl negate %k 30 %S\nEOF\n```\n\n- Disabling unprivileged user namespaces\n\nYou can use the [released PoC](https://github.com/manizada/CIFSwitch) to validate the mitigations.\n\n### Distro impact tables\n[#](#distro-impact-tables)\n\nA very non-exhaustive list of systems tested.\n\n#### Stock-exploitable\n[#](#stock-exploitable)\n\nHere, `cifs-utils`\n\nis installed by default and the default distro config (LSMs/etc.) does not stop the\nexploitation:\n\n| Target | Details |\n|---|---|\n| Linux Mint 21.3/22.3 Cinnamon | Exploitable with AppArmor active |\n| CentOS Stream 9 GNOME | Exploitable with SELinux enforcing |\n| Rocky Linux 9 Workstation | Exploitable with SELinux enforcing |\n| Kali Linux 2021.4/2022.4/2023.4/2024.4/2025.4/2026.1 headless | Exploitable with AppArmor active |\n| AlmaLinux 9.7 Workstation/Azure cloud image | Exploitable with SELinux enforcing |\n| SLES 15 SP7/SAP 15 SP7 | Exploitable with AppArmor active |\n| SLES SAP 16 | Exploitable with SELinux permissive |\n\n#### Stock-policy exploitable if cifs-utils is installed\n[#](#stock-policy-exploitable-if-cifs-utils-is-installed)\n\nExploitable under default distro config, but `cifs-utils`\n\nneeds to be installed manually:\n\n| Target | Details |\n|---|---|\n| Ubuntu 18.04/20.04/22.04 Desktop/Server | Exploitable with AppArmor active |\n| Pop!_OS 22.04 Intel/24.04 Generic | Exploitable with AppArmor active |\n| Ubuntu 24.04 Desktop minimal/full and Server | Direct `unshare` is blocked by AppArmor userns policy; exploitable through `aa-exec -p trinity` ‘\n|\n| Debian 11/12/13 netinst standard and GNOME/KDE/standard/XFCE | Exploitable with AppArmor active |\n| CentOS Stream 9 Cinnamon/KDE/MATE/XFCE | Exploitable with SELinux enforcing |\n| Rocky Linux 9 KDE/Workstation-Lite | Exploitable with SELinux enforcing |\n| openSUSE Leap 15.6 GNOME/KDE | Exploitable with AppArmor active |\n| Rocky Linux 8 GenericCloud | Exploitable with SELinux enforcing |\n| Oracle Linux 8/9 KVM | Exploitable with SELinux enforcing |\n| Amazon Linux 2023 KVM | Exploitable with SELinux permissive |\n\n#### Blocked by stock policy\n[#](#blocked-by-stock-policy)\n\nThe default distro config (LSMs/etc.) blocks exploitation even if `cifs-utils`\n\nis\npresent:\n\n| Target | cifs-utils installed by default? | Details |\n|---|---|---|\n| Ubuntu 26.04 Desktop minimal/full and Server | no | Blocked by AppArmor userns policy by default; exploitable after AppArmor userns sysctls are relaxed |\n| Fedora 40/41/42/43/44 Workstation/Server | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| CentOS Stream 10 GNOME | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| CentOS Stream 10 KDE | no | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| Rocky Linux 10 Workstation | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| Rocky Linux 10 KDE/Workstation-Lite | no | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| AlmaLinux 10.1 Workstation/Azure cloud image recipe | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| Oracle Linux 10 KVM | no | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| openSUSE Tumbleweed GNOME/KDE | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| openSUSE Leap 16.0 OEM GNOME/KDE | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| openSUSE Leap 16.0 Minimal-VM | no | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n| SLES 16 | yes | Blocked by SELinux enforcing by default; exploitable after `setenforce 0` |\n\n#### Unaffected\n[#](#unaffected)\n\nThe two tested cases where `cifs-utils`\n\nis too old to be exploitable:\n\n| Target | cifs-utils installed by default? | Details |\n|---|---|---|\n| Amazon Linux 2 KVM | no | Unaffected by this PoC: cifs-utils 6.2 lacks the namespace-switch path |\n| Kali Linux 2019.4/2020.4 | yes | Unaffected by this PoC after userns relaxation: cifs-utils 6.9 lacks the namespace-switch path |\n\n## Conclusion\n[#](#conclusion)\n\nUltimately, 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:\n\n``` php\nforged userspace cifs.spnego request\n    -> root cifs.upcall is launched by the normal request-key rule\n    -> cifs.upcall trusts fake, not-actually-kernel-originating fields\n    -> fake pid + upcall_target=app moves the root helper into an attacker-controlled namespace\n    -> NSS lookup happens before the final privilege drop\n    -> namespace-local NSS module is loaded inside the root helper, writing to sudoers.d\n```\n\nWhile the primitives themselves are not groundbreaking, the chain is pretty neat, and is much more exciting than the earlier\n“drunk” [ksmbd memory safety](/posts/drinking-llms/) findings! The graph-based approach was likely not *strictly* necessary,\nas simple Markdown memory may have sufficed, but it did seem to enable the agents to systematically burn down the potential\nexploit lanes with ease.", "url": "https://wpnews.pro/news/cifswitch-a-non-universal-linux-local-root-vulnerability", "canonical_source": "https://heyitsas.im/posts/cifswitch/", "published_at": "2026-05-28 18:18:31+00:00", "updated_at": "2026-05-28 18:34:04.903689+00:00", "lang": "en", "topics": ["large-language-models", "ai-research", "ai-tools", "ai-safety"], "entities": ["LLMs", "CopyFail", "GraphWalk", "Linux"], "alternates": {"html": "https://wpnews.pro/news/cifswitch-a-non-universal-linux-local-root-vulnerability", "markdown": "https://wpnews.pro/news/cifswitch-a-non-universal-linux-local-root-vulnerability.md", "text": "https://wpnews.pro/news/cifswitch-a-non-universal-linux-local-root-vulnerability.txt", "jsonld": "https://wpnews.pro/news/cifswitch-a-non-universal-linux-local-root-vulnerability.jsonld"}}