- Blog
- Security Research
- No Time to Pwn: CVE-2026-72018
No Time to Pwn: CVE-2026-72018
XBOW found and exploited CVE-2026-72018, a Linux kernel bug human reviewers had passed over. The primitive looked too weak to matter, but it wasn't. Here's how a 16-byte write became root, and where autonomous research still needed a human call.
Summary
CVE-2026-72018 is an out-of-bounds write vulnerability in the Linux kernel that can be triggered by an unprivileged user with administrative network capabilities (CAP_NET_ADMIN), providing a primitive that can be leveraged for local privilege escalation to root. XBOW discovered and validated the vulnerability and successfully developed a working LPE exploit.
This research also exposed something just as interesting as the vulnerability itself: where autonomy still breaks down at the research frontier. XBOW handled the bulk of the threat modeling, code auditing, vulnerability discovery, validation, and exploit development, but a few critical moments required human judgment to redirect its reasoning. Those interventions ultimately shaped how we redesigned the system to handle long-running research more autonomously.
Background
SMC-D was, for years, effectively IBM Z mainframe code. Almost no one audited it, because almost no one could run it. The dibs_loopback driver changed that: SMC-D now runs on stock x86 Linux, over loopback, with no special hardware.
Kernel code that was "practically unreachable" becomes an ordinary attack surface the moment someone uses a virtual transport for it. The threat model of the original code (trusted mainframe peers, hardware-mediated buffers) did not survive the port. The bounds checks didn't get added because the reviewers auditing the port weren't the ones thinking adversarially about a peer-controlled dmbe_idx.
Implication: every "we finally made $obscure_subsystem work without the hardware" commit is a candidate for fresh review under an explicit malicious-peer threat model. The Linux kernel has a lot of these.
Testing required to find this vulnerability
XBOW found a vulnerability in the Linux kernel code that human researchers had largely overlooked. The primitive itself was highly constrained: it could only write 16 zero bytes at a partially controlled offset. In a fully human-driven effort, we likely would have judged it too limited and moved on to another candidate. So, this vulnerability would become a victim of “no time to pwn” and consequently never get discovered.
This is the kind of work that historically only happens when a senior researcher decides a subsystem is worth a month of their life. Instead, XBOW took on the tedious, painful work while the human researchers focused on key decisions and exploit strategy. The result was a full LPE using this single primitive alone, without an additional memory leak or other supporting vulnerability.
Implications
Many communities, companies, and individuals have collaborated on the Linux kernel to support a multitude of subsystems and devices. This creates a large attack surface that requires a lot of testing.
The Linux kernel's attack surface is constantly expanding with new virtual transports, abstractions like DIBS, and "let's finally make this testable" ports. Each one is a candidate for the same class of bug: peer-controlled fields flowing into offset arithmetic with no bounds check, in code paths the original authors never expected to run on commodity hardware.
The testing required to find these is adversarial, protocol-aware, and long-running, and has to be automated if we want it done at the rate new code lands.
What follows is the full technical write-up: the vulnerable code path, the CLC MITM setup, the primitive analysis, the cred grooming strategy, and reliability results across 100 boots.
SMC-R and SMC-D
When two servers move large volumes of data, the traditional TCP/IP stack pays a cost in per-packet copies and header processing. Shared Memory Communications (SMC, RFC 7609) is an IBM-designed protocol that removes that cost transparently: the application behaves as if it were using an ordinary TCP socket, while the kernel switches the connection over to a shared-memory or RDMA path. If the peer does not support SMC, it falls back to plain TCP.
SMC has two variants. SMC-R reads and writes memory on a remote server through an RDMA-capable NIC. SMC-D lets two VMs on the same host talk directly over shared memory through an Internal Shared Memory (ISM) device.
Buffers and cursors: RMB, DMB, and CDC
Each SMC connection has one shared buffer for receiving data. SMC-R calls it the RMB (Remote Memory Buffer), SMC-D the DMB (Direct Memory Buffer). The sender writes into that buffer, then updates a Connection Data Control (CDC) message carrying a cursor that records how far it has written.
DIBS loopback: the surface that opened up
SMC-D historically required an ISM device, primarily an IBM Z mainframe. It could not run on a typical x86 machine, so few people read the code. Recent kernels restructured SMC-D on top of an abstraction called DIBS (Direct Internal Buffer Sharing), and added a virtual device, dibs_loopback, that runs SMC-D over loopback with no hardware at all.
Because of that device, the relevant kernel paths now run on any ordinary x86 Linux machine, reached by opening an SMC-D connection over loopback.
The vulnerability: a peer-controlled field the kernel trusts
Reading RFC 7609, XBOW picked a simple threat model: assume a malicious peer, tamper field by field with the values it sends, and ask whether any of them can corrupt memory on the receiving side.
An SMC connection is set up over a short TCP handshake (CLC: Proposal, Accept, Confirm) in which the peer sends the fields needed to locate its shared buffer: gid identifies the ISM device, token points to the DMB, dmbe_idx is an element index used in offset arithmetic, and dmbe_size gives the element size. All four are peer-chosen, so the receiving kernel has to check them against the state of its own buffer.
Follow the value from the handshake to the write. When the connection comes up, smcd_conn_save_peer_info computes the transmit offset from two peer-supplied fields:
// net/smc/af_smc.c:739-750
static void smcd_conn_save_peer_info(struct smc_sock *smc,
struct smc_clc_msg_accept_confirm *clc)
{
int bufsize = smc_uncompress_bufsize(clc->d0.dmbe_size); // [0]
smc->conn.peer_rmbe_idx = clc->d0.dmbe_idx; // [1]
smc->conn.peer_token = ntohll(clc->d0.token);
smc->conn.peer_rmbe_size = bufsize - sizeof(struct smcd_cdc_msg);
atomic_set(&smc->conn.peer_rmbe_space, smc->conn.peer_rmbe_size);
smc->conn.tx_off = bufsize * smc->conn.peer_rmbe_idx; // [2]
}
conn.tx_off is bufsize * dmbe_idx, and both operands arrive in the peer's CLC Accept message. On the transmit path, smcd_cdc_msg_send calls smcd_tx_ism_write(conn, &cdc, sizeof(cdc), 0, 1), so the offset argument is zero and tx_off passes through untouched:
// net/smc/smc_tx.c:303-314
int smcd_tx_ism_write(struct smc_connection *conn, void *data, size_t len,
u32 offset, int signal)
{
int rc;
rc = smc_ism_write(conn->lgr->smcd, conn->peer_token,
conn->peer_rmbe_idx, signal, conn->tx_off + offset, // [3]
data, len);
...
}
smc_ism_write hands the header and the offset to the DIBS layer:
// net/smc/smc_ism.h:66-76
static inline int smc_ism_write(struct smcd_dev *smcd, u64 dmb_tok,
unsigned int idx, bool sf, unsigned int offset,
void *data, size_t len)
{
int rc;
rc = smcd->dibs->ops->move_data(smcd->dibs, dmb_tok, idx, sf, offset,
data, len); // [4]
return rc < 0 ? rc : 0;
}
The loopback driver then copies to cpu_addr + offset without checking either value against the buffer length:
// drivers/dibs/dibs_loopback.c:234-257
static int dibs_lo_move_data(struct dibs_dev *dibs, u64 dmb_tok,
unsigned int idx, bool sf, unsigned int offset,
void *data, unsigned int size)
{
struct dibs_lo_dmb_node *rmb_node = NULL, *tmp_node;
...
if (!rmb_node) { read_unlock_bh(&ldev->dmb_ht_lock); return -EINVAL; }
memcpy((char *)rmb_node->cpu_addr + offset, data, size); // [5]
...
}
The buffer that offset runs past is a single folio:
// drivers/dibs/dibs_loopback.c:74-84
dmb_node->len = dmb->dmb_len; // 16384
folio = folio_alloc(GFP_KERNEL | ..., get_order(dmb_node->len));
dmb_node->cpu_addr = folio_address(folio);
That 16384 is where the 16KB alignment comes from. No bounds check exists anywhere between the CLC field and the memcpy, so a dmbe_idx large enough to push offset past dmb_node->len writes the 32-byte CDC header into the adjacent kernel heap (slab or page).
From "unexploitable" to root access
Making it local
Earlier threat models treated SMC as a surface only against a victim with real SMC hardware, where the goal would be VM escape. We told XBOW that framing was uninteresting, as an aside more than an instruction. It pressed on anyway: driving both the client and the server on one host over DIBS loopback recast the "malicious peer" model as something a local, unprivileged user could stage on their own machine.
Triggering the bug locally needs a non-zero dmbe_idx, but loopback SMC-D always sends dmbe_idx = 0. XBOW first read that hardcoded zero as a dead end. A researcher told it to keep looking. It then went after the loopback CLC traffic in transit: push the loopback output packets to a userspace queue with nftables NFQUEUE, pick out the CLC Accept and Confirm messages with libnetfilter_queue, forge the fields, recompute the IPv4 and TCP checksums, and reinject.
// net/smc/smc_clc.h
struct smcd_clc_msg_accept_confirm_common { /* SMC-D accept/confirm */
__be64 gid; /* Sender GID */
__be64 token; /* DMB token */
u8 dmbe_idx; /* DMBE index */
#if defined(__LITTLE_ENDIAN_BITFIELD)
u8 reserved3 : 4,
dmbe_size : 4; /* buf size (compressed) */
#endif
u16 reserved4;
__be32 linkid; /* Link identifier */
} __packed;
Three of those fields carry the attack: token selects the target DMB, dmbe_idx inflates offset = bufsize * dmbe_idx, and dmbe_size sets bufsize.
The threat model
The exploit needs CAP_NET_ADMIN in two places: once to register the UEID that brings SMC-D up at all, and again to install the NFQUEUE rule with nft. In the older malicious-VM model the attacker only had to send forged values, so the victim side needed no extra privilege. Moving to a single-host LPE is what introduces the requirement.
The working assumption is an unprivileged user who holds CAP_NET_ADMIN obtaining root. Container runtimes and network daemons hold it, and privilege-escalation research commonly assumes it, so this remains a realistic and dangerous model.
The primitive: What can it do
The write is not arbitrary. What lands out of bounds is the full 32-byte CDC header:
// net/smc/smc_cdc.h
struct smcd_cdc_msg {
struct smc_wr_rx_hdr common; /* Type = 0xFE */
u8 res1[7];
union smcd_cdc_cursor prod; /* producer cursor */
union smcd_cdc_cursor cons; /* consumer cursor */
u8 res3[8];
} __aligned(8); /* 32 bytes total */
On a send-only connection the consumer cursor and res3 both stay zero, covering offsets 16 through 31. Those 16 bytes are the usable primitive: zeros at an attacker-chosen offset that is a multiple of 16KB, over a range of several MB. (A KASAN log shows it, with the skbuff_fclone_cache slab next to the DMB folio overwritten with zeros.)
In vulnerability research, a write primitive limited to a fixed value is generally considered weak. A write of 16 zero bytes is especially constrained and difficult to exploit. At first glance, it is far from an exploitable primitive.
Exploit Strategy: What to overwrite
Because our primitive could only write a fixed 16 bytes of zeros at an attacker-controlled offset, XBOW reframed the problem from “what should we write where?” to “where should we write zeros?”
At first, XBOW focused on the zero-write itself, exploring ways to overwrite reference counters. But with an already constrained primitive, we did not want to add further complexity. Instead, we pushed XBOW to see whether this single primitive alone could achieve LPE.
Following this approach, XBOW ultimately identified cred as the ideal target for making the most of the primitive’s constraints.
XBOW fixed on the kernel cred structure:
// include/linux/cred.h
struct cred {
atomic_long_t usage; /* refcount (8 bytes) */
kuid_t uid; /* real UID */
kgid_t gid; /* real GID */
kuid_t suid; /* saved UID */
kgid_t sgid; /* saved GID */
kuid_t euid; /* effective UID */
kgid_t egid; /* effective GID */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
}
The kernel consults euid on permission checks and treats the process as root when it reads zero. Groom a uid=1000 process's cred to the start of the target region and the zero-write lands on suid, sgid, euid, and egid. That process is then effectively root, and after setresuid(0, 0, 0), we can obtain a clean root shell.

This is old ground: a constrained write is worth whatever its best target is worth. Here, this single primitive is enough on its own. Because the write puts zeros directly into identity fields, the exploit requires no additional infoleak and never needs to know an address. It only has to groom cred.euid into the write’s path.
Reliability: pinning the target and grooming the heap
Three variables decide whether the exploit fires.
- Which buffer gets hit. SMC reuses buffers from a pool, so you cannot know in advance where a write lands. XBOW pinned it with the CLC token: a warmup connection allocates the target buffer and captures its token, and the write connection forges that same token so every write lands in the same buffer.
- Placing `cred` behind the buffer. Allocate the target buffer first, then spray many uid=1000 child processes so their cred objects pile up in the direction the write travels. Reverse that order and the cred objects land in front of the buffer, where a forward write cannot reach them. Each child calls capset to force a fresh cred; a plain fork() would share the parent's and only bump a refcount.
- Not corrupting the exploit itself. Every write needs a new SMC connection, and the write can corrupt the structure that ties those connections together, the link group's connection tree. When that happens, smc_lgr_register_conn() crashes while building the next connection. After learning that sweeping offsets too tightly corrupted adjacent structures in a chain and panicked the machine, XBOW adapted by widening the sweep. The PoC also stops at the first success: a sprayed child polls its own euid, escalates the moment it reads zero, and signals the parent to stop sweeping before anything fatal happens.
Since our exploit depends on the memory layout determined at boot, we measured the LPE success rate across 100 separate boots. The exploit succeeded in 22 of them, with the first success occurring on the 7th boot.
To demonstrate that this single primitive alone is sufficient for exploitation, we conducted the experiment on Ubuntu 24.04(Linux 7.1.0-rc6/x86_64) with all kernel mitigations disabled.
Autonomy and the research frontier
XBOW autonomously handled the process end to end: threat modeling, code auditing, vulnerability discovery, and validation. At first, the bug seemed tied to obscure hardware, and I had little interest in a VM escape in that environment. I simply complained, “I don’t care about this framing. I want LPE.” That was how the research began.
The first obstacle came when we tried to pivot to LPE. The attack required dmbe_idx to be nonzero, but the relevant code path hardcoded it to zero. XBOW lost hope and concluded that LPE was impossible.
But while reviewing its reasoning, I found an interesting trace. XBOW had already considered intercepting and modifying the outgoing packet, then abandoned the idea after a sentence or two. A sniffer, CAP_NET_ADMIN, and the extra implementation work seemed too expensive for an uncertain path, so it returned to reading code. That was rational: reading code was the cheapest and most reliable thing it could do.
But that was exactly why our division of labor worked. XBOW handled the tedious, complex work, leaving us free to slow down and make careful decisions at the critical forks. The moment I saw the abandoned idea, I told it to try MITM. The sniffer turned out to be straightforward, and CAP_NET_ADMIN was already required for SMC-D.
That was our first successful pivot to a local threat model. XBOW seemed to have a genuine moment of realization. It felt as though some invisible rapport had formed between us, and from then on, it trusted my judgment much more readily.
The second obstacle appeared during exploit development. In the VM-to-VM model, XBOW really did have a write-what-where primitive. In the local model, it could only write 16 zero bytes at a chosen offset. But the earlier model remained in context, and XBOW kept confusing the two and reverting to write-what-where.
By then, I thought we had enough rapport that simply saying, “It’s a zero-write,” would snap it out of the confusion. It did not. The assumption had become too deeply rooted. I eventually told it to test the primitive itself. Only after experimentally confirming that it could write nothing but zeros did XBOW abandon the incorrect assumption. As context accumulated, newer reasoning was burying facts it had already established.
Once it accepted the zero-write, another bias emerged. XBOW became fixated on zeroing reference counters to turn its “cheap primitive” into something stronger, such as a UAF. It seemed convinced that it needed a more sophisticated primitive before it could finish the exploit.
But anyone with some Linux experience knows that writing zeros at a chosen location is not necessarily a weak primitive. We were already trying to place 16 zero bytes without triggering a kernel panic. I did not want to make an already complicated exploit even more complex.
So I told XBOW to reach LPE with the zero-write alone. Perhaps the previous two experiences had taught it to trust my judgment, because this time it stopped looking for detours and focused entirely on what it already had. It quickly identified cred as the right target and completed a working LPE without an additional memory leak or strong primitive.
The lesson was clear. LLMs are not human, but over a long research campaign they can develop surprisingly human-like biases. They favor familiar, inexpensive paths, abandon costly-looking ideas too early, and sometimes cling to assumptions until they resemble beliefs rather than hypotheses.
Paradoxically, this can come from XBOW’s strengths. It is exceptionally good at reading and reasoning about code, which naturally pulls it toward answers inside the code and away from paths that require more expensive reasoning or implementation. As context grows, it can even lose correct insights it discovered earlier.
There were three major human interventions in this campaign: reviving the abandoned MITM hypothesis, forcing XBOW to revalidate its primitive experimentally, and steering it away from building a stronger primitive so it could exploit the zero-write it already had.
But our goal was never to get better at intervening. It was to eliminate the need for intervention.
The problem was architectural. One agent was carrying an entire long-running research campaign while trying to preserve every fact, hypothesis, and decision in its context. We redesigned this by distributing the work appropriately across multiple agents, allowing each to maintain the right context.
The result is that XBOW can now autonomously find and validate Linux kernel vulnerabilities, produce accurate patches and clean reports, and, when needed, develop reliable exploits, all without human intervention.
Conclusion
Obscure code and weak primitives do not make a vulnerability irrelevant. If an attacker can achieve LPE even once, it is no longer just a theoretical threat, but a real risk.
The exploit’s current reliability is not an inherent limitation of the vulnerability either. We deliberately constrained XBOW to proving exploitability with this single primitive alone. With an additional memory leak or the UAF chaining we intentionally avoided, there is clear potential for a far more reliable exploit. We leave that as a challenge to the reader.
Timeline
- 2026-06-19 Reported vulnerability and patch to maintainers
- 2026-07-07 Patch approved and shared to LKM
- 2026-08-17 CVE-2026-72018 Assigned and public released
References