Firezone logo light
Jamil Bou Kheir
Jamil Bou Kheir

The Linux TCP RCE is worse than we thought

If you run any kind of TCP service exposed to the internet, chances are your ghasts were flabbered reading the news about CVE-2026-98432, the use-after-free vulnerability in Linux kernels going back to 4.4 (10+ years ago!).

Flaws like this are why we advocate never exposing services to the internet unless you absolutely must. But I'll save you the sales pitch - the reason I'm writing this post is that we're seeing lots and lots of straight-up incorrect information swirling around the internet regarding what in the heck this vulnerability actually is and how to (partially) mitigate it while you (still!) may be waiting for your distro to ship a kernel patch.

The tl;dr:

  • Dropping IPv4 packets with IP options is a good idea (see our sample XDP program), but does not fully mitigate. Traffic through tunnels, loopback sockets, and containers can still trigger the bug, as explained below.
  • This is not a Multipath TCP bug, nor does disabling MPTCP (such as with net.mptcp.enabled=0) resolve the underlying vulnerability
  • The only full fix is to upgrade your kernel to a patched version

Ok so what's the bug and how is it triggered?

Quick recap on IP options and TCP

TCP carries most traffic on the internet today - it's serving this very blog post. TCP requires a three-way handshake to be established before any information flows:

The TCP three-way handshakeA client opens a TCP connection to a server in three steps. The server starts out listening. The client sends a SYN, and from the moment it arrives the server holds the connection in a half-open state. The server answers with a SYN-ACK, and when it arrives the client considers itself connected. The client confirms with a final ACK, and when that arrives the server promotes the half-open connection to a full connection.ClientconnectingconnectedServerlisteninghalf-openconnectedSYNSYN-ACKACK
The TCP three-way handshake.

Until the handshake is established, the connection is said to be in a half-open state. During this state, the kernel keeps a request socket around to manage state until the connection completes or times out (this is much lighter than a full socket). If the client's SYN carried IP options, the request socket also holds a copy of them.

What are IP options?

Every IPv4 packet starts with a 20-byte header with the basics, like where the packet came from and where it's going. That header can carry up to 40 more bytes of IP options, which ask network hops to do something extra with the packet.

Record Route, for example, asks every router along the way to write its address into the packet (try this with ping -R), and Timestamp asks each one to note the time. Other options let the sender pick the path it takes or attach a security label to it.

The IPv4 headerAn IPv4 packet starts with a fixed 20-byte header, laid out in rows of 4 bytes. Bytes 0 to 3 hold the version, the header length (IHL), DSCP and ECN, and the total length. Bytes 4 to 7 hold the identification, flags, and fragment offset. Bytes 8 to 11 hold the time to live, the protocol, and the header checksum. Bytes 12 to 15 hold the source address and bytes 16 to 19 the destination address. From byte 20, the header can carry 0 to 40 bytes of options, and the payload, such as a TCP SYN, follows the header. The header length field and the options are highlighted.IPv4 headerbit 0bit 8bit 16bit 24bit 310VersionIHLDSCP / ECNTotal length4IdentificationFlagsFragment offset8TTLProtocolHeader checksum12Source address16Destination address20Options, 0 to 40 bytesPayload, such as a TCP SYN
IHL counts the header in 4-byte words. Anything above 5 means options follow the fixed 20 bytes.

IP options date back to the early days of the internet, when it was just a small trusted research network. Almost nothing uses them today - nearly all ISP and cloud providers drop packets with options.

Linux still accepts options like Record Route and Timestamp, though, and when a SYN arrives with options, the kernel saves a copy so that its SYN-ACK can mirror them back to the client.

That saved copy is where the bug is.

How is MPTCP involved?

Multipath TCP is not the core issue, so disabling it won't protect you completely.

The original discovery by Anthropic used a Multipath TCP listener as its way in. Why? MPTCP does make the race easier to win, since the attacker can force the memory to be freed immediately, but SYNs to that listener still need to carry IP options.

Golang >= 1.24 enables MPTCP on Linux TCP listeners by default. The Golang team mentioned disabling it, noting this is however a "fragile mitigation" and the real fix is to upgrade to a patched kernel.

Based on the above, flawed advice seems to have spread (at least throughout my social feeds) that sysctl -w net.mptcp.enabled=0 would protect you. It might make successful exploitation more difficult, but it's no mitigation - upgrade your kernel instead. If you can't do that for whatever reason, drop IPv4 packets with options, as explained in the stopgaps below.

So what's the issue exactly?

The issue occurs only on multicore systems and specifically under the following scenario:

  1. A server has replied to a client's SYN with a SYN-ACK and is now awaiting the client's final ACK.
  2. CPU A receives that final ACK, while at the same moment CPU B receives a SYN with the same sequence number.

When this happens, it's possible the following occurs:

  1. CPU A begins to promote the request socket to a full connection, handing it the IP options and moving it into the connection table for future packets.
  2. CPU B looks up the duplicate SYN and finds the request socket before CPU A has finished.
  3. CPU B starts building the SYN-ACK to send back to the client. As part of this, it starts copying the IP options from the request socket.
  4. At this same moment, the new connection is torn down, freeing the memory CPU B is still reading to set up IP options.

When it can be triggered

The race only matters if the request socket actually holds options. That happens in two ways:

  1. The SYN carries IP options. The kernel saves any options it finds on an incoming SYN. Any host that accepts IPv4 TCP connections is exposed, as long as those SYNs can reach it. Note that local sockets are just as susceptible as remote sockets here.
  2. The host labels the connection itself. When SELinux or Smack is active and NetLabel maps the peer to CIPSO, the kernel attaches a CIPSO option to the connection. On these hosts, an ordinary SYN with no options is enough.

SELinux and Smack are Linux security modules that tag processes, files, and sockets with security labels, then use those labels to decide who can access what. CIPSO (Commercial IP Security Option) is an IP option that carries those labels across the network, so the receiving host knows how sensitive the traffic is, like "Secret" or "Top Secret". NetLabel is the kernel subsystem that decides which peers get labeled this way.

Thankfully, the second case is rare. CIPSO labeling is mostly found in government and defense networks running SELinux with an MLS policy, and in embedded systems running Smack, such as Tizen and Automotive Grade Linux. Most servers run that need this kind of thing run AppArmor, or SELinux with no CIPSO mapping instead.

To check, run netlabelctl map list and look for CIPSOV4 entries. Here's the output on my default Ubuntu 26.04 server install:

> sudo netlabelctl map list
domain:DEFAULT,UNLABELED

Stopgap 1: Drop IPv4 packets with options at ingress with XDP

Since practically nothing legitimate needs IP options, dropping them at the firewall or network ingress is the first step towards a reasonable stopgap. If you want to do this on the host itself, drop-ipv4-opt does that using XDP + eBPF, so it should be very fast. It drops any IPv4 packet whose header length field (IHL) is above 5, and passes everything else up the stack into the kernel.

Note: we weren't able to get any IPv4 packets with IP options through to our test VMs in Azure. Others have noted similar results with GCP and AWS, but this is no guarantee. Dropping them yourself is cheap insurance.

Why dropping IP options at ingress is only a partial mitigation

The XDP filter above only sees packets coming into the host through the physical NIC, and only sees the outer IPv4 header. That leaves the following gaps:

  • Tunnels. A SYN with options inside VXLAN, GRE, IP-in-IP, or WireGuard passes the filter, then gets unwrapped inside the host with its options intact.
  • Local traffic. Loopback traffic never crosses the physical interface, and any unprivileged process can set IP options with setsockopt(IP_OPTIONS). Ordinary sockets can't send the duplicate SYN the race needs, which makes this harder to exploit, but not impossible.
  • Containers and namespaces. Container traffic over veth pairs and bridges also skips the physical interface. A user who can create their own user and network namespace gets CAP_NET_RAW inside it and can send any packet they like, while the kernel memory they corrupt is shared with the whole host.
  • The NetLabel/CIPSO case above is also not covered - the kernel attaches that option itself, so dropping packets with options doesn't help there.

If you use Firezone: as of Gateway 1.6.5, Linux GUI Client 1.5.21, and Linux Headless Client 1.5.16, Firezone strips options from IPv4 packets in both directions, closing the Tunnels gap above. See the changelog for details.

Stopgap 2: Also drop IPv4 packets with options with nftables

If you also want to catch traffic that never reaches the physical NIC, you can drop packets with IP options at the kernel level with nftables. This is a bit more expensive than XDP, so it might be a good idea to apply both.

The following rules drop any IPv4 packet whose header length field (IHL) is above 5 on the host's input, output, and forwarding paths, including loopback and normally routed tunnel traffic:

table inet drop_ipv4_options
delete table inet drop_ipv4_options

table inet drop_ipv4_options {
    chain input {
        type filter hook input priority -310; policy accept;
        ip hdrlength > 5 counter drop
    }

    chain forward {
        type filter hook forward priority -310; policy accept;
        ip hdrlength > 5 counter drop
    }

    chain output {
        type filter hook output priority -310; policy accept;
        ip hdrlength > 5 counter drop
    }
}

Save that to drop_ipv4_options.nft (it's also in the drop-ipv4-opt repo under nft/) and load it:

sudo nft -f drop_ipv4_options.nft

The rules live in their own table, so they won't touch your existing ruleset, and the file is safe to load more than once. This doesn't survive a reboot. To make it permanent, include the file from wherever your distro loads nftables rules at boot (/etc/nftables.conf on Debian / Ubuntu, with nftables.service enabled).

To check that it's loaded and see how many packets were dropped:

sudo nft list table inet drop_ipv4_options

To remove it:

sudo nft delete table inet drop_ipv4_options

Note: These rules do not cover traffic confined to other network namespaces, such as loopback inside a container, or bridge traffic that bypasses these hooks. The NetLabel/CIPSO case above is also not covered - the kernel still attaches that option itself, so these rules don't help there.

The actual fix

The fix, commit e3f33b0a1d89 ("ipv4: free inet_opt and ireq_opt after an RCU grace period"), changes two kfree() calls to kfree_rcu(), so the options are only freed once every CPU still reading them has finished:

net/ipv4/af_inet.c (inet_sock_destruct)
-       kfree(rcu_dereference_protected(inet->inet_opt, 1));
+       kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);

net/ipv4/tcp_ipv4.c (tcp_v4_reqsk_destructor)
-       kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1));
+       kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu);

Because it changes how the memory is freed rather than how the options get there, it closes every problematic code path above, including the gaps. It first shipped in Linux 7.3 and is being backported to stable kernels.

As of this writing (Nov 10, 2026), some distros still don't have the patched kernel available in stable. Until that lands, we recommend:

  • Keeping IPv4 packets with IP options dropped, bearing in mind the gaps listed above
  • Double-checking that NetLabel doesn't map any peers to CIPSO
  • Upgrading to a patched kernel from the unstable or proposed channels for your distro instead, or building your own patched kernel from source if you can't wait.

Engineers on call

If you're stuck waiting on a kernel patch, unsure whether you're exposed, or just want a second pair of eyes on your mitigations, we're happy to help, whether or not you use Firezone. We have extra hands on deck over the next few weeks, so reach out and one of our engineers will get back to you.

Firezone Newsletter

Sign up with your email to receive roadmap updates, how-tos, and product announcements from the Firezone team.

By checking the box below, you agree to receive communications from Firezone. You can unsubscribe anytime.

To deliver your service, we need your permission to store and process your personal data. We care about your privacy. Learn how we handle your data in our Privacy Policy.