TLDR: OVSwrap (CVE-2026-64531) is a non-universal (but broad) Linux LPE found by giving LLMs the tools to reason through memory safety issues’ geometry (coupled with CIFSwitch-discovery-style graph reasoning tools). Read on for affected distros, mitigations, and vulnerability details.
Background #
In CIFSwitch, we gave models the tools to build and navigate semantic graphs – and got a nice multihop logical vulnerability chain in return. I like deterministic logic bugs: they are elegant and reliable. Memory bugs, OTOH, almost always involve grooming, indeterminism, and the chance to crash the host if you don’t place things right. It also doesn’t help that (open) LLMs, in my experience, are just not that good at reasoning about memory issues (finding an overflow is one thing, but thinking ‘geometrically’ to groom the memory for an exploit is another).
My taste preferences aside, I figured – why not try and solve LLMs’ blindspots’ with tools once again? I settled on something extremely basic: forcing the hunter agents to keep a persistent state of the relevant geometric structures via ASCII diagrams, at each state of iteration. E.g., as a trivial example for heap/allocators (pretend tcaches, arena *bins, etc., do not exist):
address slot contents
──────────────────────────────────────────────────────────────
0x1000 S0 [ A0 ]
0x1100 S1 [ A1 ]
0x1200 S2 [ FREE ]
0x1300 S3 [ OVERFLOWING BUF ]
0x1400 S4 [ FREE ] <- we want to place an object here
0x1500 S5 [ A5 ]
0x1600 S6 [ A6 ]
0x1700 S7 [ FREE ]
──────────────────────────────────────────────────────────────and
C100 free list:
HEAD
|
v
[S7 @ 0x1700] -> [S2 @ 0x1200] -> [S4 @ 0x1400] -> NULL
^
|-- target holeand the equivalent for within-buffer visualizations (which is what will ultimately matter below).
Proprietary LLMs were clever enough to track these via text files; some open-source ones resisted, so I just straightjacketed them into bolting on the relevant diagrams via grammar-like rules (in general, you can do really fun things when you control the token generation!). Coupling this with the earlier graph building/traversal tool, this got us…
The vulnerability #
…a memory bug in the kernel’s Open vSwitch, but – in a nice surprise – one that works with the determinism of a logic bug!
TLDR #
Open vSwitch accepts a list of “actions” from userspace and validates/rewrites some of the actions into a larger internal form. These internal actions are stored as Netlink attributes, whose length field is only 16 bits wide.
Now, the total internal action stream is allowed to grow past 64 KiB. But an individual nested action attribute (an attribute and all of its recursive children) still needs to fit in that 16-bit length field. Pre-fix, Open vSwitch did not check that second limit.
As a result, an attacker can submit a valid action (e.g., CLONE) containing hundreds of, say, small conntrack actions. The kernel expands those actions until the generated action is larger than 65,535 bytes, then stores that length in the 16-bit nla_len, causing it to wrap to a small value. Later code trusts the wrapped length, advances by it, and resumes parsing from the middle of the generated conntrack data.
Critically, parts of that conntrack data – labels and timeout names – are controlled by the attacker. And, because the wraparound redirects parsing to a deterministic offset within the same contiguous, attacker-created action stream, forged action headers/payloads can be placed exactly where parsing resumes. Once per-kernel-build offsets are known, no grooming is required.
I reported the issue to security@kernel.org and the Open vSwitch maintainers on June 19, 2026. The fix made it into the stables on July 24. Per the coordinated embargo with linux-distros@, we agreed to publish the writeup/PoC on July 28, 2026, 6am UTC, so that the affected system owners can apply the now-published patches or apply other mitigations. Big thanks to Ilya Maximets and Aaron Conole for OVS-side triage and review and to linux-distros@ for partnering on coordinated disclosure.
Open vSwitch basics #
Open vSwitch, or OVS, is a software network switch commonly used by virtualization, cloud, container, and SDN platforms. On Linux, the implementation is split between
ovs-vswitchdin userspace, which decides what flows should exist, and- the kernel, which executes those flows quickly for each packet.
A flow contains a key describing what traffic it matches and a list of actions to perform. Actions do things like:
- send the packet through an output port
- change packet or tunnel fields
- pass the packet through conntrack
- clone the packet and run another list of actions
- sample traffic
- choose different actions based on packet length
Userspace sends these flow keys and action lists to the kernel through Generic Netlink.
Importantly, the attacker does not need much to reach the vulnerable code – no existing OVS bridge, no running ovs-vswitchd, no host-level CAP_NET_ADMIN. All you need is a CAP_NET_ADMIN in the user namespace that owns the network namespace*.
An ordinary user can therefore use an unprivileged user namespace to create a new network namespace, receive CAP_NET_ADMIN over that namespace, and create a private OVS datapath inside it with a plain unshare -Urn. This leaves many distros vulnerable in their default unprivileged-user-namespaces-enabled configuration (see the distro impact tables below).
* Note
: this means the vulnerability is reachable from, e.g., a container with the appropriate
CAP_NET_ADMIN
,
potentially
enabling container escapes as well. Our PoC does not cover this attack vector.
Vulnerability details #
Reaching the path as an unprivileged user #
The OVS flow/datapath families use namespace-aware Generic Netlink permissions. So, as discussed above, a process doesn’t need CAP_NET_ADMIN in the initial user namespace, only a CAP_NET_ADMIN in the user namespace that owns the network namespace with the OVS datapath. The simplest scenario is unprivileged user namespaces:
ordinary host user
|
| unshare -Urn
v
uid 0 + CAP_NET_ADMIN inside a new user/network namespace
|
| create private OVS datapath
v
OVS flow installation interfaceCrucially, on systems that ship OVS as a loadable and installed module, resolving an OVS Generic Netlink family by name can load the module. OVS registers matching module aliases for those families, so it does not matter if it wasn’t already loaded – it will be loaded if available! This is why almost all tested distros are vulnerable: the vast majority ship OVS as a loadable + installed module, as you can see in distro impact tables.
Exposing the wrap-around #
Netlink attributes are small records with a type and a length (maximum of 65,535):
struct nlattr {
__u16 nla_len;
__u16 nla_type;
};OVS also uses these attributes for its internal generated action stream. When it starts a nested action – e.g., a CLONE – it reserves a header, copies the child actions, and then goes back and fills in the final nested length. Before the fix, that last step was basically:
static inline void add_nested_action_end(struct sw_flow_actions *sfa,
int st_offset)
{
struct nlattr *a =
(struct nlattr *)((unsigned char *)sfa->actions + st_offset);
a->nla_len = sfa->actions_len - st_offset;
}So, a->nla_len would expect at most 16 bits, but actions_len needn’t be that
short – and nothing ensured that the subtraction fit into the nla_len.
The unsafe assignment itself had existed for 13 years until the fix, but older kernels had a conditional 32 KiB limit on the generated action stream. It acted as another layer of defense, preventing a nested action from reaching the 65,536-byte wrap point.
But the 32 KiB limit was removed in March 2025, as it was causing unpredictable failures: OVS actions can grow or shrink while being converted to their internal form. So, removing the check on the total length made sense, but it had the side effect of no longer preventing any nested attributes from reaching 65,536 bytes. This finally rendered the decade+-old bug exploitable, with the change getting backported into most upstream stable trees at the time.
The fix #
The kernel fix keeps support for a total generated action stream larger than 64 KiB, but rejects any individual generated nested attribute that cannot fit in nla_len:
static inline int add_nested_action_end(struct sw_flow_actions *sfa,
int st_offset)
{
struct nlattr *a;
u32 attr_len;
...
attr_len = sfa->actions_len - (u32)st_offset;
if (attr_len > U16_MAX)
return -EMSGSIZE;
a = (struct nlattr *)((u8 *)sfa->actions + st_offset);
a->nla_len = attr_len;
return 0;
}Adding the size check itself is simple, but safely unwinding after the failed check was the messy part: a generated action list can already contain resources such as tunnel destinations or nested conntrack actions by the time OVS discovers that an outer wrapper is too large. The patch therefore gets the recursive builders to free their partially constructed tails in reverse order before truncating the failed wrapper.
The corresponding changelogs for the first upstream-fixed releases are:
The exploit #
Now, let’s walk through how this wrap-around primitive ends up exploitable by the PoC.
OVS code uses each action’s nla_len to find the next top-level action in the
sw_flow_actions stream. So, if the real CLONE is larger than 65,535 bytes but its stored length wraps to a small value, the parser stops walking the CLONE early and resumes while it is still inside its payload. If the bytes at that wrapped endpoint look like another OVS action, the parser processes them as one. (If this is confusing, hold tight, we are about to dive in with some visuals.)
So, the PoC needs something that:
- expands enough to push the generated
CLONEthrough the 65,536-byte boundary, - contains attacker-controlled bytes where the wrapped parser lands
This is where conntrack comes in!
The unreasonable effectiveness of conntrack #
Conntrack is very useful for “economically” achieving the wrap-around – when OVS copies it, the kernel builds an internal struct ovs_conntrack_info containing a bunch of fields (the parsed zone, helper, labels, timeout, template, NAT state, etc.). This blows up a few-byte action to 164 bytes (at least on x86-64).
In the end, the header, an internal exec attribute, and 400 generated CT actions
result in 4 + 8 + 400*164 = 65,612 bytes. This wraps nla_len to
65,612 mod 65,536 = 76, or 0x004c, stored in nla_len of the header.
But the underlying buffer still contains all 65,612 bytes! Visually:
Sample sw_flow_actions buffer:
offset region contents
─────────────────────────────────────────────────────────────────────────
+0x0000 CLONE header [ nla_len = 0x004c | CLONE ]
+0x0004 CLONE payload [ generated EXEC ]
+0x000c generated CT #0 [ CT fields ... ]
+0x004c still inside CT #0 [ FAKE ACTION | attacker bytes ... ]
^
|-- wrapped length sends the
parser here
+0x00b0 generated CT #1 [ CT fields ... ]
... [ CT #2 ... CT #399 ]
+0x1004b final stored byte [ ... ]
+0x1004c end of used extent end of the real CLONE
─────────────────────────────────────────────────────────────────────────Later OVS consumers walk the stored stream with the normal Netlink iteration rules:
nla_for_each_attr(a, actions, actions_len, rem) {
switch (nla_type(a)) {
...
}
}The iterator trusts a->nla_len. It sees a CLONE whose length is supposedly 76,
advances 76 bytes, and treats whatever comes next as another top-level action.
But byte 76 is not the end of the real CLONE! It is still inside the first
generated conntrack object:
Actual ownership:
[ CLONE @ +0x0000 ........................................ +0x1004c )
Parser walk:
HEAD
|
v
[CLONE @ +0x0000, len=0x004c] -> [FAKE ACTION @ +0x004c] -> [...]
^
|-- target landing point,
still inside generated CT #0The PoC arranges for that landing point to fall in attacker-controlled CT labels or timeout bytes, then fills those bytes with a fake OVS action.
Conntrack is therefore doing two jobs: its expansion causes the wrap, and its controlled fields provide the fake actions that OVS sees after the wrap. The latter unlocks the critical read and decrement primitives.
Leaking the first kernel pointer #
Leaking the first kernel pointer
The first fake action is an OUTPUT with nla_len = 512, placed at +0x004c inside CT #0. Since nla_len includes the four-byte action header, its declared payload begins at +0x0050 and extends through the generated CT actions that follow:
+0x004c [ fake OUTPUT header ][ declared payload ...................... )
|
v
+0x0050 [ rest of CT #0 ][ generated CT #1 ][ generated CT #2 ][ ... ]
|
|-- contains a real FTP-helper pointer
|
+0x024c end of declared OUTPUTThe PoC asks the following CT actions to use the FTP conntrack helper, so their
generated ovs_conntrack_info objects contain real pointers to it. Then, when OVS dumps the flow, it believes the full 512-byte span belongs to the fake OUTPUT action – copying the whole thing back to userspace. That includes the real FTP-helper pointer… which will be useful shortly.
Turning a fake tunnel SET into a read
#
We then place a fake tunnel SET action at the wrapped landing point; its payload contains an attacker-chosen tun_dst pointer. Then, when OVS dumps the flow, it treats tun_dst as a pointer to real tunnel metadata and serializes the fields from it back to userspace.
For a desired kernel byte at address A, the PoC chooses tun_dst so that one
of the fields OVS serializes lands on A:
target byte A
^
| OVS reads a known tunnel field here
|
P + field_offset = A
^
|
`-- forged tun_dst PIn other words, tun_dst = target_address - field_offset, where field_offset is the per-kernel-build offset from tun_dst to the tunnel field OVS will serialize.
A fresh fake SET is used for each byte. Depending on the address, the PoC reads through a tunnel TOS, TTL, port, or IPv4 field.
Using this read primitive, it follows the leaked FTP-helper pointer to:
- the helper’s owning module;
- the module’s
kobjecttype pointer; and - the randomized kernel base.
After that, finding other kernel objects becomes trivial.
Turning teardown into a decrement #
The same fake tunnel SET also provides the constrained write primitive. When OVS frees the action, it releases the forged tunnel destination:
dst_release((struct dst_entry *)ovs_tun->tun_dst);dst_release() decrements the destination’s reference-count field at a fixed
offset from tun_dst.
The PoC chooses:
tun_dst = target_address - dst_refcount_offsetso that the apparent destination refcount overlaps the target word:
forged tun_dst P target word T
| |
v v
[ apparent dst_entry ............ ][ fsuid = 1000 ]
^
|-- apparent refcount overlaps T
P + dst_refcount_offset = T
dst_release(P) -> decrement *TOne malicious flow deletion performs one decrement:
[1000] -> [999] -> [998] -> ... -> [1] -> [0]This is not a fully arbitrary write, but it is more than enough to get to root.
From a decrement to host root #
At this point, the PoC has a full kernel read and the decrement primitive. It can now:
- find the randomized address of
init_pid_ns - look up the host writer’s PID in the initial PID namespace’s IDR
- recover the writer’s
task_struct - read the writer’s credential pointer, and
- repeatedly apply the decrement primitive to the credential.
On modern kernels, the PoC repeatedly decrements the writer’s fsuid and fsgid to zero. On older kernels whose dst_entry uses __refcnt, it instead decrements zero-valued capability words once, wrapping them to 0xffffffff and giving the writer the capabilities needed to switch to UID/GID 0.
At this point, the process (in the host namespace) whose credential is corrupted performs the write against /etc/sudoers.d or /etc/sudoers as necessary. The OVS trigger is the only part running inside the private namespaces.
Logic-bug-grade reliability #
Interestingly (to me, a memory bug disliker) this is a kernel memory-corruption vulnerability without any sort of heap/allocator/etc. grooming required.
The important values (two kernel symbol offsets, the size and layout of struct ovs_conntrack_info, field offsets, etc.) are typically static for one exact kernel build. These can be trivially derived from System.map, readable kallsyms, kernel and module BTF, or matching distro debug packages – single-digit hours of LLM labor.
For this reason, and given the patches and other mitigations are available, I’m releasing the x86-64 PoC with pre-derived records for ~800 kernel builds, as the defensive value of checking for exposure IMO outweighs what the attackers can trivially derive themselves.
Are you affected? + Mitigation #
The bug reachability pre-reqs #
The bug itself is reachable on any pre-fix kernel that got the cap-removing commit from March 2025, whether directly or via backports, assuming you have OVS reachable/loadable.
For official upstream kernels, the affected ranges are:
| Kernel series | Affected releases | First fixed release |
|---|---|---|
| 5.15.y | 5.15.180–5.15.211 | 5.15.212 |
| 6.1.y | 6.1.132–6.1.177 | 6.1.178 |
| 6.6.y | 6.6.84–6.6.144 | 6.6.145 |
| 6.12.y | 6.12.20–6.12.96 | 6.12.97 |
| 6.13.y | 6.13.8–6.13.12 | EOL; no upstream stable fix |
| 6.14.y–6.17.y | All releases | EOL; no upstream stable fix |
| 6.18.y | 6.18.0–6.18.39 | 6.18.40 |
| 6.19.y | All releases | EOL; no upstream stable fix |
| 7.0.y | All releases | EOL; no upstream stable fix |
| 7.1.y | 7.1.0–7.1.4 | 7.1.5 |
Vendor-specific kernels behave variously:
- some RHEL-derived 5.14 kernels contain the enabling change and are affected
- some SUSE 6.12 kernels retain the old cap and are not affected, etc.
A process can trigger the bug when it has CAP_NET_ADMIN over an attacker-controlled network namespace on an affected OVS-enabled kernel. As briefly discussed earlier, this may be:
- an ordinary local user using unprivileged user namespaces (the simplest scenario)
- a container process given the appropriate namespace-local capabilities, or
- another already-privileged network-management process.
And so while a container escape is theoretically possible with this primitive, I did not pursue that direction with the PoC. But just as with the earlier CIFSwitch finding, someone else may put one together.
PoC pre-reqs #
For the released PoC, the additional requirements are:
- OVS conntrack support + FTP conntrack helper,
- sudo installed, and
- an x86-64 build covered by the embedded records (or enough symbols/BTF to derive the build-specific values via
paholeon the fly).
SELinux/AppArmor did not block any exploit steps in my testing (not counting AppArmor blocking unprivileged user namespace spawning on some of the Ubuntu releases as seen below).
All of this is a lot to keep track of, so check out the distro impact tables below to get a practical picture, and simply upgrade if at all possible.
Mitigation #
Install a kernel containing the fix if you can. If that’s not possible:
- If OVS is not needed, unload, and blacklist
openvswitchif possible - Disable unprivileged user namespaces to remove the normal ordinary-user route (but again, this wouldn’t protect against containers or processes that already have the relevant
CAP_NET_ADMIN) - If none of the above are feasible, Use the emergency BPF guard included alongside the PoC. See the README in
bpf-mitigationfor more details.
Distro impact tables #
The following is a non-exhaustive list of exact systems tested.
“Default config” includes ordinary updates on the distribution’s normal kernel track.
Exploitable in default config #
| Target | Details |
|---|---|
| AlmaLinux 9.7 Workstation/Azure cloud, 9.8, 10.1 Workstation/Azure cloud, and 10.2 x86-64/x86-64-v2 | Exploitable as tested. |
| Alpine Linux 3.22.4/3.23.4/3.24.1 Cloud and 3.22.5/3.23.5/3.24.1 LTS/virt | Exploitable as tested. |
| Amazon Linux 2023 KVM (6.1/6.12/6.18 kernel tracks) | Exploitable as tested. |
Arch Linux monthly (linux/linux-lts/linux-zen) |
Exploitable as tested. |
| CentOS Stream 9 Cinnamon/GNOME/KDE/MATE/XFCE and 10 GNOME/KDE | Exploitable as tested. |
| Debian 12/13 | Exploitable as tested. |
| Fedora 40 Workstation/Server | Upstream stable 6.8 never received the OVS cap-removal change. Stock 6.8.5-301.fc40 rejects the oversized action, but an ordinary same-track update to 6.14.5-100.fc40 is exploitable. |
| Fedora 41 Workstation/Server | Upstream stable 6.11 never received the OVS cap-removal change. Stock 6.11.4-301.fc41 rejects the oversized action, but an ordinary same-track update to 6.17.10-100.fc41 is exploitable. |
| Fedora 42/43/44 Workstation/Server | Exploitable as tested. |
Gentoo amd64 cloud image and stable gentoo-kernel-bin 6.1/6.6/6.12/6.18 branches |
Exploitable as tested. |
| Kali Linux 2026.1 | Exploitable as tested. |
| Linux Mint 22.3 Cinnamon | Exploitable as tested. |
| NixOS 24.11/25.05/25.11/26.05 | Exploitable as tested. |
| openSUSE Tumbleweed GNOME/KDE | Exploitable as tested. |
| Pop!_OS 22.04 Intel/24.04 Generic | Exploitable as tested. |
| Rocky Linux 9/10 KDE/Workstation/Workstation Lite | Exploitable as tested. |
| Ubuntu 22.04 Desktop minimal/full and Server | Exploitable as tested. |
| Ubuntu 24.04 Desktop minimal/full and Server | Direct unshare is blocked by AppArmor’s user-namespace policy, but the aa-exec -p trinity trick enables exploitation. |
Exploitable after some tweaks #
| Target | Details |
|---|---|
Arch Linux monthly (linux-hardened) |
Stock kernel.unprivileged_userns_clone=0 blocks reachability. Setting it to 1 makes the tested 7.1.4-hardened1-1-hardened kernel exploitable, though naturally this defeats the purpose of the hardening. |
| Linux Mint 21.3 Cinnamon | Stock 5.15.0-91-generic does not carry the affected upstream 5.15 backport and rejects the oversized action. Installing the optional linux-generic-hwe-22.04 and booting into 6.8.0-134-generic makes it exploitable. |
| Oracle Linux 8/9/10 KVM | The stock images lack the required Open vSwitch/conntrack module files. Installing and loading the missing module packages makes them exploitable. |
| Ubuntu 26.04 Desktop minimal/full, Server, and generic/AWS/Azure/GCP/GKE/Oracle cloud kernel tracks | Blocked by the stock AppArmor unprivileged-user-namespace policy. Exploitable after setting kernel.apparmor_restrict_unprivileged_userns=0. |
Not exploitable across available tested distro kernels #
These kernels retain the older generated-action limit, do not contain the enabling backport, or otherwise lack the vulnerable tested path.
| Target | Details |
|---|---|
| Amazon Linux 2 KVM | Unaffected by this path: upstream 5.10.y never received the OVS cap-removal change. Tested 5.10.257-254.1015.amzn2 retains the cap. |
| Debian 11 | Unaffected by this path: upstream 5.10.y never received the OVS cap-removal change. Tested 5.10.0-44 retains the cap. |
| openSUSE Leap 16.0 OEM GNOME/KDE and Minimal-VM | Upstream 6.12.y received the enabling change in 6.12.20, but interestingly the tested SUSE 6.12.0-160000.35 does not carry that backport and rejects the oversized action. |
| Rocky Linux 8 GenericCloud | The distro 4.18 kernel line never received the OVS cap-removal change. Tested stock and updated kernels retain the cap. |
| Ubuntu 18.04 Desktop/Server | Upstream 4.15.y never received the OVS cap-removal change. Tested 4.15.0-213 retains the cap. |
| Ubuntu 20.04 Desktop minimal/full and Server | The 5.4 GA line never received the change. Although upstream 5.15.y received it in 5.15.180, tested Ubuntu HWE 5.15.0-139 does not carry that backport. |
Conclusion #
- Even dumber models can be made much better at reasoning through memory safety issues and exploits by giving them the right tools.
- If you can run a kernel with a strict whitelist of kmods you need, you probably should, as this immediately closes off a lot of attack surface.
- Relatedly, as we saw here, in CIFSwitch, and countless other recent (and not) vulnerabilities, it is a good idea to keep unprivileged user namespaces disabled unless you actively require them. They open up an incredible amount of attack surface; stay tuned for more on this in the coming weeks.