Blog

Frag Gap (CVE-2026-53362): 15 Bytes That Turn a UDP Socket Into Root

Frag_Gap_(CVE-2026-53362)
CVE / Docker / Linux / Security

Frag Gap (CVE-2026-53362): 15 Bytes That Turn a UDP Socket Into Root

An ordinary process on your server opens a UDPv6 socket, chains a couple of splice() calls from a pipe into it — and within seconds it’s reading and writing arbitrary physical memory. Not through a bug in your application. Not through some vulnerable systemd service. Through code in the kernel that counts the same 15 bytes twice: once as part of the data, once as something that already lives somewhere else. That confusion is called Frag Gap — CVE-2026-53362, a local privilege escalation to root, with a working exploit and a full technical writeup already published by researchers physicube and qwerty on July 20, 2026. Red Hat rates it CVSS 7.0 (vector AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H), CWE-131 — incorrect calculation of buffer size; Amazon Linux’s own advisory puts it slightly higher, at 7.8. That gap of a few tenths is normal for kernel CVEs — different CNAs score the vector a bit differently — but both land in the High bracket with full impact on confidentiality, integrity, and availability. The disagreement is over a fraction of a point, not over how serious the bug is. A patch exists, the exploit is public, but CISA hasn’t flagged this as active exploitation in the wild — the bug was found and disclosed through kernelCTF, Google’s kernel vulnerability research competition.

What makes this one specific is that the bug lives in the UDP corking path for IPv6 — code present on every Linux server with CONFIG_IPV6=y, which is the overwhelming majority of modern distributions. The trigger needs no special privileges: an ordinary unprivileged local user can walk to root in a handful of syscalls.

WHAT UDP CORKING AND FRAGGAP ACTUALLY ARE

UDP corking is a mechanism that batches several send() calls into one datagram before it hits the wire. You turn it on with the UDP_CORK flag: while the socket is “corked,” data piles up in a kernel buffer instead of going out immediately. It’s an optimization — fewer packets, less header overhead.

The trouble starts when the accumulated data crosses a fragment boundary — the maximum size that fits in one network packet (MTU minus headers). The function __ip6_append_data() counts the bytes that spilled past that boundary from the previous skb (socket buffer — the kernel structure that holds a network packet) and calls them fraggap. Those bytes need to move into the linear area of the next skb — meaning they should be copied in as actual data, not treated as something external.

This is where the confusion happens. There are two ways to allocate memory for a new skb: the ordinary non-paged path, and the paged path, used for MSG_MORE, NETIF_F_SG, or large fragments. The non-paged branch accounts for fraggap correctly — the alloclen variable includes those bytes. The paged branch doesn’t:

datalen = length + fraggap;
fraglen = datalen + fragheaderlen;
pagedlen = 0;

/* ... */

else {
    alloclen = fragheaderlen + transhdrlen;
    pagedlen = datalen - transhdrlen;
}

datalen already includes fraggap. But alloclen — the size actually allocated for the linear area — doesn’t account for it. pagedlen, meanwhile, inherits the extra bytes from datalen. The result: the copy variable collapses to a negative value, exactly -fraggap. That used to trigger a -EINVAL error — the kernel simply refused the operation. But commit ce650a166335, added to support MSG_SPLICE_PAGES (zero-copy data transfer straight from a pipe into a socket), lifted that check specifically for the negative-copy case. From that point on, the bug became genuinely exploitable.

WHERE THE OUT-OF-BOUNDS WRITE ACTUALLY LANDS

From here the code ignores the fact that fraggap is data with nowhere to go, and copies it into the linear area anyway:

data = skb_put(skb, fraglen - pagedlen);
data += fragheaderlen;

if (fraggap) {
    skb->csum = skb_copy_and_csum_bits(
        skb_prev, maxfraglen,
        data + transhdrlen, fraggap);
}

skb_put() moves the buffer’s tail exactly up to skb->end — the end of the memory actually allocated for data. Right past that boundary sits skb_shared_info — the packet’s metadata structure: fragment count, flags, a destructor pointer. The fraggap bytes get copied straight into that region — past the edge of what was actually allocated for data.

In physicube and qwerty’s writeup, the math works out like this: a 320-byte IPv6 routing header gives fragheaderlen = 360. The first skb comes out at 1287 bytes. The fragment boundary sits at 1272. The difference — exactly 15 bytes of fraggap. Those 15 bytes land precisely at the start of skb_shared_info, offsets +0x00 through +0x0e.

HOW IT IS EXPLOITED

The attack rides on two splice() calls into a corked UDPv6 socket. The first moves 919 bytes from a pipe — with UDP and routing headers accounted for, the resulting skb lands at exactly 1287 bytes, 15 over the fragment boundary. UDP_CORK keeps it queued instead of sending it right away. The second splice() adds another 20 bytes — and it’s this step where the copy recalculation goes negative, creating a new skb with fraggap = 15.

Of those 15 available bytes, the attacker only needs one — the byte at offset +0x02, the nr_frags field in skb_shared_info. That’s the counter for how many page fragments are attached. Normally it’s zero on a fresh skb, but this particular region of memory isn’t fully cleared: __finalize_skb_around() only zeroes the structure up to the dataref field, and the frags[] array — where page descriptors actually live — sits after it and can still hold data from whatever previously occupied that memory. By setting nr_frags = 1 through the OOB write, the exploit makes the kernel believe frags[0] is a valid page descriptor, even though that page was already freed.

What follows is a classic grooming technique: the exploit first builds a valid page descriptor in the same object cache through a series of tee() calls that push the source pipe page’s refcount up to nine, so it survives the freeing and reuse of that memory. When the socket closes, the skb destructor calls put_page() on however many pages nr_frags says are there — including the one activated through the OOB write. That leaves a dangling page: the physical memory is back in the allocator, but the attacker’s pipe_buffer still points to it.

Next the exploit calls mmap() and touches it in a way that gets the allocator to hand that same physical page back out as a page table entry (PTE). Once that happens, a write through the pipe into the old dangling page becomes a write into the process’s own PTE — meaning the attacker can change the physical address their own virtual memory points to. That gives arbitrary physical memory read and write from userspace.

The final step is finding the kernel variable core_pattern in physical memory (it defines which program runs when a process crashes and dumps core) and overwriting it with something like |/proc/%P/fd/666 %P. When a child process then crashes with SIGSEGV, the kernel runs that command as root.

WHAT HAPPENS NEXT — DEPENDS ON SYSTEM CONFIGURATION

There’s one condition for triggering it, but it’s a hard one: CONFIG_IPV6=y. If the IPv6 stack is disabled at build time, the IPv6 attack path simply doesn’t exist. But the bug had an IPv4 twin with the same underlying logic error, fixed in a separate patch — commit eca856950f7c. Disabling IPv6 alone isn’t enough on an unpatched kernel: the IPv4 path is confirmed exploitable too, something Sultan Alsawaf of the CIQ team explicitly confirmed in the writeup.

Containerization by itself doesn’t save you either. The vulnerability lives in the kernel’s network stack, not in userspace code, so an unprivileged user inside a Docker container without extra syscall restrictions (a seccomp profile blocking splice() or network operations) reaches root on the host exactly the same way — as long as the container shares the host kernel, which it almost always does with Docker.

TIMELINE

The fraggap accounting bug entered the kernel on July 12, 2022 — commit 773ba4fe9104, “ipv6: avoid partial copy for zc,” part of the zero-copy work. A year later, on August 2, 2023, commit ce650a166335 removed the check against a negative copy for the MSG_SPLICE_PAGES case — the exact moment that made the bug actually triggerable rather than a latent logic error sitting quietly in the code. For three years, that combination just waited for someone to turn it into an exploit.

On May 15, 2026, physicube and qwerty reported the vulnerability directly to [email protected]. The IPv6 fix (commit 736b380e28d0) landed in the net tree on June 21, then merged into mainline on June 25. The Linux kernel CNA published CVE-2026-53362 on July 4. On July 14, the researchers notified Openwall’s closed linux-distros list — the standard coordinated-disclosure procedure for distributions. The full writeup and working exploit went public on July 20, 2026, and can be found on GitHub in the qwerty-po/security-research repository.

WHY IT MATTERS

For hosting providers running multi-tenant environments, this is one of the worst bug categories there is: an unprivileged user on shared hosting, on a VPS with a shared kernel, or in any container without a tight seccomp profile gets root on the host — meaning access to every other customer on that same machine. No special software, no specific application configuration needed — just a working kernel with IPv6 enabled and the ability to open a UDP socket, which is usually allowed by default.

For an ordinary sysadmin running dedicated servers, the risk is lower but not zero: if there’s even one unprivileged shell account on the box — for developers, for CI/CD runners, for any restricted service account — this bug turns it into full root. Given that the exploit is already published alongside the writeup, the gap between disclosure and adapted variants showing up in real attacks is measured in days, not weeks.

There’s a separate reason this one is worth worrying about: the nature of the bug itself. This isn’t a “forgot to check the length” buffer overflow. It’s an accounting mismatch between two code paths that handle nominally the same bytes differently. Bugs like that are systematically harder to catch with static analysis and code review, because each branch looks correct on its own — the problem only shows up when you compare the two execution paths side by side.

UPDATE

The IPv6 patch is available in commit 736b380e28d0, the IPv4 one in commit eca856950f7c. Per the official advisory, both fixes are included in stable kernel versions 6.1.177, 6.6.144, 6.12.95, 6.18.38, 7.1.3, and 7.2-rc1.

On Debian and Ubuntu you can check your installed kernel version like this — the command prints the version of the package built into your system:

uname -r

If the version is below whichever patched release applies to your branch, the kernel is vulnerable. From there it’s a standard package manager update:

apt-get update && apt-get upgrade linux-image-$(uname -r | sed 's/-generic//')

One important catch: as of this article’s publication, per Ubuntu’s official security page, the core kernel packages — linux, linux-aws, linux-gcp, linux-azure — on both 24.04 LTS and 26.04 LTS still show a status of “Vulnerable.” A fixed package hasn’t shipped yet. Check the current status at ubuntu.com/security/CVE-2026-53362 before considering this closed: your next apt update will pull the patch in automatically once Canonical ships a built package, but until then you need a temporary workaround.

While the patch hasn’t landed in your distro yet, a stopgap is to block the vector through nftables, restricting UDPv6 socket creation for unprivileged users, or — if IPv6 isn’t in use on your infrastructure at all — disabling it via sysctl. One thing to keep in mind: fully disabling IPv6 on Debian and Ubuntu needs both of the following parameters, or part of the stack stays active:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1

The first command disables IPv6 on every interface that already exists. The second sets it as the default for interfaces that show up later — say, after you bring up a new Docker bridge. Skip the second one, and a fresh container will come back up with IPv6 active again. This is a workaround, not a substitute for the patch: once an updated kernel is available for your distro, install it, and if your services actually need IPv6, turn it back on.

For a more surgical option, if disabling IPv6 entirely isn’t on the table, you can restrict access to splice() through a seccomp profile for containers and service accounts that don’t need that syscall — though this isn’t described as a recommended measure in the official advisory, so treat it as extra defense-in-depth rather than your primary protection.

CONCLUSIONS

If you’re a hosting provider running shared infrastructure or managing a multi-tenant container cluster, this is priority number one this week. Check uname -r on every node, compare it against the patched versions (6.1.177 / 6.6.144 / 6.12.95 / 6.18.38 / 7.1.3 / 7.2-rc1), and if your distro’s update isn’t out yet, disable IPv6 through both sysctl parameters right now — not after the update shows up in the repository.

If you administer dedicated servers with no untrusted local users, the risk is lower — but don’t skip this: CI/CD runners, service accounts, and any restricted shell access all count as local users as far as this bug is concerned. Patch on your regular update cycle, but don’t put it off.

If you’re a hosting customer, no direct action is needed on your end — that’s your provider’s job — but it’s worth asking them directly when CVE-2026-53362 gets closed on your infrastructure, especially if you’re sharing a node with other customers.

Leave your thought here

Your email address will not be published. Required fields are marked *