The patch is out, so why isn't my server safe yet?
HOTSecurity27 min read

The patch is out, so why isn't my server safe yet?

One question about Linux kernel TCP flaw CVE-2026-98432: what the fix waits for, package vs upstream commit, installed vs running kernel, containers.

The kernel security advisory says “fixed.” On what day can you write down that your own server is safe? This article follows that single question through one flaw, CVE-2026-98432. The answer is not the day you installed the package. It is the day you have passed all five gates below, one after another. Each gate comes with an experiment our editorial team ran ourselves or an official document. All dates and times are UTC, and the facts about the incident come only from the official trackers and advisories published by Debian, Ubuntu, and Red Hat. Explanations of principles based on official documentation, the experiments we ran, and our own analysis are labeled “editorial commentary” and “editorial experiment.” We do not cover attack steps or code, and we did not run any experiment that reproduces the flaw itself.

Five gates a patch has to cross

First, the bare outline of the incident. The fact table below contains what all three official pages have in common.

Flaw
A use-after-free in the Linux kernel's IPv4 TCP. When a retransmitted connection request and a socket migration are handled at the same time, the socket option memory is freed while it is still being used (Red Hat CVE page)
Score
Both Ubuntu and Red Hat rate it 9.8 (Critical) on CVSS 3.1, vector AV:N/AC:L/PR:N/UI:N, classified as CWE-825
Fix
Free the option memory with kfree_rcu() instead of kfree(). Upstream commit e3f33b0a1d89d738e2b913dab519226eaf76e15b, included in 7.3-rc7 according to the Debian page
Disclosure
Ubuntu page published Oct 15 (updated Oct 30); Red Hat security bulletin RHSB-2026-016 on Oct 30

“A patch exists” only means that the fix line above is true. For your server to be safe, the following five things have to be true, in order.

  1. You know exactly what the flaw is, so you can judge whether your server sits on that path.
  2. You understand the principle by which the fix actually blocks the flaw. If you don’t know what the fixed line is waiting for, you have no basis for believing “it’s fixed.”
  3. Your distribution has released a package build that carries the fix into your kernel series, and the package on your server is that build.
  4. That package is installed, and the server has been restarted so that kernel is actually running.
  5. Every other environment that uses the same kernel (containers, nodes, the boot images in your image registry) is in the same state.

Each gate produces false reassurance in a different way. The first two are matters of understanding; the other three are about matching different lists (the package list, the boot record, the infrastructure inventory).

First gate: what one SYN leaves behind inside the server

To see where the flaw lives, you first need to see what the server kernel builds in memory while a TCP connection is being set up. This section is editorial commentary, and the handshake description follows RFC 9293.

RFC 9293 defines connection setup as a three-way handshake. When a server in the LISTEN state receives a SYN, it sends back a SYN-ACK and enters SYN-RECEIVED, and it becomes ESTABLISHED only when the client’s final ACK arrives. The RFC defines SYN-RECEIVED as the state of waiting for an acknowledgment after having received and sent a connection request. Every connection that lingers in this state occupies something in server memory. RFC 4987 explains that because a server creates connection state for each incoming SYN, the number is limited, and the behavior when an implementation hits that limit is not standardized. The kernel documentation (ip-sysctl) also notes that one request socket in the SYN_RECV state uses about 304 bytes.

Linux represents this half-open connection with a lightweight object called a request socket, and only when the final ACK arrives does it create the child socket that the application actually uses. This structure is the stage for this flaw, because the two sockets end up sharing information that came from the same SYN.

One piece of that shared information is the star of this article: IPv4 options. RFC 791 defines the IHL (Internet Header Length) field of the IP header as the header length in 32-bit words, with a minimum valid value of 5. Five words is 20 bytes, the basic header with no options. When options are attached, IHL becomes greater than 5, and an option is either a single byte of type (end, No Operation) or a type, length, and data sequence. The options RFC 791 defines include Record Route (type 7), which records the path, Loose Source Route (131) and Strict Source Route (137), where the sender specifies the path, and Internet Timestamp (68), which records times. The same RFC states that options must be supported by all IP implementations. So the option-handling code is a path that any normal kernel necessarily has. The kernel documentation also says the IPv4 accept_source_route setting decides whether source-routing options are accepted, with a default of true for routers and false for ordinary hosts, so some options are filtered depending on configuration.

Let’s clear up a common misunderstanding here. The TCP options defined in RFC 9293 (MSS, window scale, SACK permitted, timestamps, and so on) and IP options are different things at different layers. The SYN of an ordinary connection almost always carries TCP options and almost never carries IP options. According to the official trackers, this flaw occurs in the object that stores IP options. To show the two kinds of options side by side, our team captured SYNs over loopback ourselves.

The experiment environment was a disposable Alpine container (the kernel is the host’s LinuxKit 6.12.5), with a server socket and a client talking over loopback inside the same container. Nothing touched the flaw; we made one ordinary connection and one connection carrying 4 bytes of IP options. First, the SYN and SYN-ACK captured from the ordinary connection.

IP (tos 0x0, ttl 64, id 9684, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.44544 > 127.0.0.1.9000: Flags [S], seq 2119922374, win 65495,
    options [mss 65495,sackOK,TS val 331999809 ecr 0,nop,wscale 7], length 0
IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.9000 > 127.0.0.1.44544: Flags [S.], seq 1157465599, ack 2119922375, win 65483,
    options [mss 65495,sackOK,TS val 331999809 ecr 331999809,nop,wscale 7], length 0

What you see inside options [...] is the TCP options. MSS, SACK permitted, timestamps, and window scale fit on one line, and the total length is 60 bytes: a 20-byte IP header plus a 40-byte TCP header (20 basic, 20 options). Next is the result of attaching a 4-byte IP option (three No Operations and an end marker) to the client socket and connecting.

IP (tos 0x0, ttl 64, id 49698, offset 0, flags [DF], proto TCP (6), length 64, options (NOP,NOP,NOP,EOL))
    127.0.0.1.46060 > 127.0.0.1.9000: Flags [S], seq 1672936102, win 65495,
    options [mss 65495,sackOK,TS val 331981633 ecr 0,nop,wscale 7], length 0

It is the same SYN, but options (NOP,NOP,NOP,EOL) now appears separately on the first line and the length is 64 bytes. The parentheses hold the IP options and the brackets hold the TCP options. In this experiment, the server kernel accepted this SYN without trouble and finished the handshake, and the connection was established in 0.000 seconds. These options are filler bytes that do nothing, not values meant to touch the flaw. The point is that when a server kernel receives such a SYN, there is a normal path that reads the options and stores them in the request socket, and that the data passing through that path is 4 bytes larger than in an ordinary packet. Those 4 bytes are also the entry condition for this flaw.

Stacked bar chart comparing the byte composition of an ordinary SYN and a SYN with IP options Figure 1. Byte composition of two SYNs captured on the same loopback (editorial experiment, tcpdump length 60 and 64)

The figure above breaks the two packets into their components. Only the bottom bar has the extra 4 bytes in red. At the packet level, a SYN with options looks noticeably different, so when we get to temporary defenses later, these 4 bytes become the filter’s criterion.

Second gate: two hands holding the same pointer

Now we follow the cause described in the Debian tracker. The option is stored in the request socket’s ireq_opt field when the SYN arrives. At the moment the connection completes, tcp_v4_syn_recv_sock() creates the child socket, and according to the tracker, this function does not copy the request socket’s options but only moves the pointer to the child socket. The result is a state in which the request socket and the child socket point at one option object through the same address. Seen as an ownership transfer, there is no problem as long as only one side touches it at a time.

Looking at the IPv6 side makes the meaning of this design clear. Both Debian and Ubuntu say that tcp_v6_syn_recv_sock() clones the options and hands the clone to the child, so the first problem applies to IPv4 only. With a clone, each side has its own share, so if one side frees its copy, the other is unaffected. On the IPv4 side, avoiding the copy came at the price of making the owner unclear.

The problem is timing. According to the Debian tracker, request sockets are now handled without the listener lock. So while one CPU completes the connection with the final ACK, another CPU can build a SYN-ACK from the same request socket in response to a SYN the client sent again. Retransmission is normal TCP behavior. According to the kernel documentation, the default for the SYN-ACK retransmission count on the receiving side (tcp_synack_retries) is 5, the last retransmission comes after 31 seconds, and the final timeout is 63 seconds. That means that during this time, events involving the same request socket can arrive on several CPUs. The child socket has no SOCK_RCU_FREE, and its destructor inet_sock_destruct() frees the options immediately with kfree(). If the SYN-ACK assembly side then reads those options in ip_options_build() after the child is gone, it reads memory that has already been returned. The Ubuntu description says the same thing.

The figure below draws this race in order. From the left are the peer, CPU A, the option object, and CPU B, and the two red arrows mark the problem window.

Sequence diagram showing the order of work among two CPUs and one option object Figure 2. A simplified sequence of the race window in the pre-fix handshake (editorial diagram, based on the Debian and Ubuntu tracker descriptions)

There are two things to read from the figure. First, the side that frees the object has finished its job normally, and the reading side was also doing its job normally. It is not that either side’s code is written wrongly; the order in which the two meet is unprotected. Second, the green dotted line at the bottom is the one line the fix changes. We will look at what this line waits for later.

Redrawing the same structure centered on owners gives this. In the figure below there is one option object and several hands pointing at it.

Graph showing the sockets pointing at the option object and the difference between the two ways of freeing Figure 3. One option object, its holders, and the two ways of freeing it (editorial diagram)

In the left area, the request socket and the child socket point at the same object, and the SYN-ACK assembly on another CPU, drawn with a dotted line, is also reading it. On the right, kfree() returns the memory immediately, while kfree_rcu() waits for the reads already in progress and then returns it.

The second branch of the fix is also understood with the same figure. According to Debian and Ubuntu, in environments that use request socket migration, meaning net.ipv4.tcp_migrate_req=1 or an eBPF program using the BPF_SK_REUSEPORT_SELECT_OR_MIGRATE method, the original request socket and its clone share the same ireq_opt. If tcp_v4_reqsk_destructor() then frees it immediately in the same way, the same problem occurs. The kernel documentation says tcp_migrate_req is a setting that moves pending or accepted child sockets to another listener on the same port when a listener closes, with a default of 0. The value our team read on the container kernel above was also 0.

$ cat /proc/sys/net/ipv4/tcp_migrate_req
0

When this value is 0, one of the conditions for the second branch does not apply. But that this setting is 0 does not mean the first branch is safe. The first branch arises from the race between a retransmitted SYN and connection completion, regardless of the migration feature.

The reason the Red Hat advisory says the flaw requires precise timing also comes from this figure. The problem only occurs in the brief gap where the two CPUs’ ordering goes out of step, so sending the same packets does not give the same result every time (editorial commentary). That is why a quiet server log is no proof of safety. A day on which the problem did not occur and a server on which it cannot occur are different things.

What happens when freed memory is read, with a small toy

“Reading freed memory” is abstract. To show what happens when you read it, our team wrote a small C program that runs in user space. It is a toy that has nothing to do with the kernel and is not a reproduction of the kernel flaw. It only shows how an ordinary memory allocator behaves when one object is pointed to from two places and one side frees it.

struct opt *a = malloc(sizeof *a); strcpy(a->data, "OPT-OF-CONN-A");
struct opt *stale = a;                /* a second hand holding the same address */
free(a);                              /* the owner returns it immediately */
struct opt *b = malloc(sizeof *b);    /* the allocator gives that slot to someone else */
strcpy(b->data, "SECRET-OF-B");
printf("%s\n", stale->data);          /* read through the freed pointer */

This is the output from compiling and running it on Alpine (musl).

stale==b ? yes (same slot reused)
holder reads through stale pointer: SECRET-OF-B

The allocator immediately handed the freed slot to another use, and reading through the stale pointer returned the new owner’s value instead of the original value (OPT-OF-CONN-A). Two things come out of this output. First, code that reads freed memory may not crash right away, and the program keeps running without knowing that the value it read is wrong. Second, that wrong value depends on who newly acquired the slot. In the kernel, network buffers, sockets, and file structures share the same memory pools, so what a stale pointer reads may be some other use inside the kernel. This is why use-after-free is classified as a risk of both a simple crash (denial of service) and memory corruption. The Red Hat advisory also distinguishes that the realistic impact is a crash, that memory corruption is also possible, and that arbitrary code execution would require precise heap control.

The kernel documentation describes a tool that catches this class of error. KASAN (Kernel Address Sanitizer) is, by the documentation’s definition, a dynamic memory safety checker that finds out-of-bounds accesses and use-after-free. Generic mode uses one eighth of kernel memory as shadow memory, where one shadow byte indicates whether 8 real bytes are accessible: 00 means all accessible, and a negative value means inaccessible, such as a redzone or freed memory. The compiler inserts __asan_load*() and __asan_store*() checks in front of every memory access. According to the documentation, only Generic KASAN has a quarantine that delays the reuse of freed objects. Reflecting this documentation onto this article’s toy, the immediate reuse we saw above is the opposite of the situation the checker tries to prevent. The report includes the stack of the bad access, the stack of the allocation site, and the stack of the free site together. This is because fixing a use-after-free requires knowing both who read and who freed first. The fact that this CVE’s fix description names two functions (inet_sock_destruct(), tcp_v4_reqsk_destructor()) reads the same way.

One caveat. KASAN is slow and uses a lot of memory, so it is usually run on debugging kernels (the documentation describes Generic mode as being for debugging, with significant performance and memory overhead). According to the documentation, the arm64 hardware tag-based mode (HW_TAGS) has low overhead and is an option for production use, but it reports only the first error. On a kernel that is not a debug build, you cannot expect a KASAN report to appear in the logs and tell you when this flaw occurs. That is one more reason a clean log is not evidence of safety (editorial commentary).

Third gate: what the fixed code waits for

The fix is short. At the two places that return the option memory, kfree() was changed to kfree_rcu(). But unless you understand why this change eliminates the problem, “it’s fixed” is just a belief. So we read the RCU description in the kernel documentation.

RCU (Read-Copy-Update) is a synchronization method that the documentation describes as one where the side doing the removal waits until all readers already inside a read-side critical section have finished, and only then returns the memory. Readers mark their read-side section with rcu_read_lock() and rcu_read_unlock(), and according to the documentation, data accessed inside is not reclaimed until the section ends. The removing side first removes the pointer so that new readers can no longer reach the object, then either waits with synchronize_rcu() or schedules a callback with call_rcu(). The documentation says synchronize_rcu() waits until the read-side sections that were already in progress on all CPUs have ended, and does not wait for readers that start afterward. This waiting time is the grace period. The documentation also notes that if the callback is merely a kfree() call, it can be written as the single line kfree_rcu(old_fp, rcu). The fix for CVE-2026-98432 is exactly this one line.

Substituting this flaw’s picture into that description shows the effect of the fix. In Figure 2 above, CPU B was in the middle of reading the option object. After the fix, even if the child socket goes away and kfree_rcu() is called, the return is postponed until CPU B, which was already inside a read-side section at that moment, leaves the section. Put the other way around, this fix rests on the premise that the reader reads the object inside an RCU read-side section. Changing only the freeing side has no effect if there is a path where the reader reads outside the section. Even if the fix looks small at two lines, that one line is interlocked with the reader’s rules (editorial commentary).

We also confirmed this with a user-space toy. The second part of the earlier C program has the removing side cut the pointer first (blocking new readers) while a slow reader holds the object, wait until the reader count reaches 0, and then free. It is not real RCU but a mimic that counts readers, meant to let you see the same sequence.

reader still sees: OPT-OF-CONN-A
free after readers done, waited about 120 ms

The reader saw the original value normally until it finished its work, and the free happened about 120 ms later. It is the same object and the same two hands as in the first toy; only the timing of the free changed. This waiting is the value of kfree_rcu(). Because memory is held during the grace period, the return is delayed by that much, and the documentation also says the call_rcu() family needs its call rate to be limited. And a kernel with the fix is code that frees at a different time from the earlier kernel. This difference connects to the topic of the next gate. A fix is written once upstream, and each distribution re-ports it to fit its own kernel series.

Fourth gate: between the upstream commit and your package

Even if the fix is right in principle, what arrives on your server is not the upstream commit but a package built by the distribution. This is where the numbers mislead people.

The Debian tracker says the upstream fix commit went into Linux 7.3-rc7. But in the status table on the same page, the fixed packages are 6.1.190-1 in bookworm and 6.12.113-1 in trixie. Since the kernel series are 6.1 and 6.12, these are not packages built from upstream 7.3 source but packages that carry the fix over into the source of that series (backports). Within the same distribution release, 6.1.176-1 is recorded as vulnerable and 6.1.190-1 as fixed. Both packages are Debian 12 and both are 6.1. Neither the release name nor the kernel series number can tell them apart; only the package build number can (editorial commentary).

The figure below shows the same facts as a grid. Each row is a combination of a release and a package build, and only one cell is lit per row.

Heatmap showing vulnerable, in progress, fixed, and not affected states by distribution release and package build Figure 4. State by release and package build as recorded by the official trackers as of Oct 30 (Debian, Ubuntu, Red Hat)

There are three things to read from the figure. First, looking at Debian’s pairs of rows, the same release appears in both a vulnerable cell and a fixed cell. That means status attaches to the build, not to the release. Second, for Ubuntu, 22.04, 24.04, and 26.04 are all in progress (WIP), while 20.04 is vulnerable and 14.04 is not affected, so the cells split. Even within one distribution, a release whose support has ended can be left without a fix. Third, Red Hat lists the affected products as RHEL 8, 9, 10 and OpenShift, marks the status as in progress, and had no erratum at the time of the advisory. The status of these cells moves over time. The values in this article are records as of Oct 30, and on the day you read it, each tracker’s current record is the reference.

This grid asks where to put your server’s version number and which cell it falls in. The practical procedure is this.

  1. Write down the exact package build: uname -r shows the running kernel release, and a package manager query shows the installed package build. The two can differ, so write down both.
  2. Find the same cell in the tracker: Find your release and build number on the distribution's security page and pick the cell. Debian uses a table per release, Ubuntu uses status per release, and Red Hat uses the RHSB and erratum as the reference.
  3. Cross-check against the change history: Read the package changelog to see whether the CVE number appears. Because this is a changelog match rather than a number comparison, it avoids the illusion of version numbers.

One more thing. Comparing against upstream version numbers is meaningful only when you use a kernel you built yourself rather than a distribution package. In that case you have to check the source directly to see whether the upstream commit is in your source tree. It does not end with the single line “7.3-rc7 or later,” because the question is whether the stable-series branch you use has received this fix.

Fifth gate: the installed kernel and the running kernel

Even if you have confirmed that the package is the right build, you are not done. A kernel is not a running program but code loaded into memory at boot. Installing a package creates new kernel files on disk and changes the bootloader entries, but the kernel already running stays as it is. Until you restart, the server is on the old kernel.

The simplest way to tell what is installed from what is running is to put the two values side by side.

uname -r                      # kernel release currently loaded in memory
# Debian family: list installed kernel image packages
dpkg -l 'linux-image-*' | grep '^ii'
# RPM family: list installed kernel packages
rpm -q kernel
last -x reboot | head -3      # time of the last restart

If uname -r equals the release of the newest package in the list, and the last restart time is later than the package installation time, that server is running the new kernel. If even one of these is off, the server is installed but not applied. Separate from the “work in progress” (WIP) the Ubuntu tracker lists in its status cell, this mismatch is common on the Debian side too, where fixed builds are already out. A server with automatic installation configured easily ends up with the latest package but a delayed restart, running the old kernel (editorial commentary).

For servers that are hard to restart, some distributions and vendors provide live patching. It works by replacing functions in the running kernel’s memory, so a live-patched server behaves like the fixed version without a restart. However, that a live patch covers this CVE holds only when the vendor has released a patch for this CVE. Using a live patch service does not mean it covers every CVE, so you have to look for the CVE number in the service provider’s advisory. So even for a server that uses live patching, you can only write that it has passed this gate after finding the number in the vendor’s CVE list.

Looking at gates 4 and 5 together gives one fact. A safe server state holds only when two different ledgers are both right: the package ledger saying the fixed build is installed, and the boot ledger saying it booted into that kernel. If you write a report from the package management ledger alone, you skip the fifth gate.

How to count the servers that share a kernel

Gate 5 was about one server. The last gate is a matter of how you count servers. Containers share the host’s kernel (editorial commentary). Our team ran an experiment to confirm this by eye. Inside the disposable container from earlier, we read the operating system distribution information and the kernel release.

$ uname -r
6.12.5-linuxkit
$ cat /etc/alpine-release
3.24.3

The container image’s user space is Alpine 3.24.3, but the kernel is the host-side LinuxKit 6.12.5, unrelated to that image. No matter how many times you rebuild the image with the latest security patches, the value of uname -r does not change. The package list inside a container image either has no kernel or has one that is not used for execution. So for a flaw caused by the kernel like this CVE, it is not resolved even if an image scanner reports “no vulnerabilities” (editorial commentary).

This also shows up in the Red Hat advisory. The advisory lists RHEL and OpenShift Container Platform as affected products, and notes that higher-level products such as RHEL CoreOS, OpenStack Platform, and Virtualization may also be affected. Because the nodes of a container platform use the host kernel, the platform product names appear on the affected list. The counting problem then changes like this. What to inspect is not the number of deployed services but how many different kernels are running. Even if 1,000 services run on 100 nodes, the unit that decides vulnerability is the kernels of the 100 nodes. Conversely, if one service spans three kinds of nodes, that service sits on three different states.

In an operating model where nodes are rebuilt and replaced, the question moves one step earlier. The criterion is whether the boot image used to launch new nodes (cloud image, golden image) contains the fixed kernel. If you replace nodes without fixing the image, the vulnerable kernel comes back up. For managed node services in the cloud, the timing of when the provider swaps the kernel differs by service. The Ubuntu tracker shows the same structure: cloud-derived packages (linux-aws, azure, gcp, oracle, gke, and so on) are listed on separate lines, with most supported releases in progress and older releases marked vulnerable. A derived kernel has its own status line separate from the base kernel. So reading the status of Ubuntu 24.04 once cannot describe all 24.04 servers. You read the right line only if you know which kernel package (linux, linux-aws, linux-hwe, and so on) your server uses.

Virtual machine environments are similar. The guest kernel is managed by the guest, so updating the hypervisor side leaves the guest server’s kernel unchanged. Conversely, fixing the guest does not fix the host. Since this flaw is in the kernel on the receiving end of TCP, the first job is to draw a map of which kernel each service that accepts external TCP connections sits on.

Roughly ten items are enough for this map: server name, kernel package name and build, last restart time, the container platform running on top, and the boot image name. There are few items, but they are scattered across different ledgers (package management, boot records, the image registry, node pool definitions), so gathering them is the hard part.

What you can do at the entrance while there is no patch, and its limits

For servers with no fixed kernel yet, or where a restart cannot be scheduled, Red Hat published a temporary filter. According to the advisory, only the patch is a complete fix, the filter may interfere with traffic the business needs and should first be verified in a test environment, and the rule is deleted after patching. The rule takes the form of counting and dropping IPv4 TCP packets in nftables whose IP header length exceeds 5 words. The IHL concept we saw at the second gate becomes the rule’s condition as is, because an IHL greater than 5 means options are attached.

Our team checked directly whether this rule works as intended. In the same disposable container we created a table with a different name (hotfix_demo) and, using a rule that only counts, made one ordinary connection and one connection with options.

table inet hotfix_demo {
	chain g {
		type filter hook input priority filter - 10; policy accept;
		ip protocol tcp ip hdrlength > 5 counter packets 4 bytes 232
	}
}

We made two connections, and the counter shows 4 packets and 232 bytes. The rule counts only packets with IP options attached, so these are the packets from the connection that carried options. Both connections were established in 0.000 seconds, and nothing was dropped at this stage. Next we added drop after counter and made the same two connections again.

plain connected in 0.000s
opt failed: TimeoutError after 3.0s
... ip protocol tcp ip hdrlength > 5 counter packets 3 bytes 192 drop

The ordinary connection was established as before, and the connection with options failed with a timeout after waiting 3 seconds. The counter recorded 3 packets. That is the number filtered each time the SYN with options was retransmitted. This confirmed that the same rule picks out only SYNs with options at the entrance, and that those SYNs do not reach the server socket. We removed this rule with nft delete table and confirmed rc=0. Since a temporary rule is something you apply, it is safer to record the removal procedure as well. The 4 bytes painted red in Figure 1 above are the only signal this filter looks at. Whether the IHL value is 5 or 6 is exactly the rule’s condition.

There are also limits to know before applying the rule (editorial commentary).

  • A per-server filter only filters packets coming into that server. If you drop option packets at an upstream load balancer or firewall, you can cover many servers at once. Conversely, if the upstream device passes options through as they are, the rule is needed on each server. Believe that an upstream device already drops option packets only when you have confirmed it through device settings and counters.
  • Legitimate traffic that uses options is dropped along with it. This includes diagnostic tools that use options such as RFC 791’s Record Route or Timestamp, and some special equipment integrations. The rule’s condition is “has options,” so it does not distinguish normal from abnormal.
  • This filter addresses the IPv4 TCP entrance. Because the first branch of the flaw comes from the IPv4 option path, it reduces that entrance, but it does not fix the code that is the cause. The Red Hat advisory’s statement that the complete fix is the patched kernel sums up this limit.
  • Checking the socket migration setting is a separate matter. The second branch applies when tcp_migrate_req=1 or when there is an eBPF program for migration. The command that reads the value, sysctl net.ipv4.tcp_migrate_req, does not change any state, and ss -tnlp shows the listeners.

Here RFC 4987’s discussion of SYN state helps once more. The document explains that SYN cookies do not allocate state in SYN-RECEIVED, and points out cases where SYN cookies are incompatible with TCP options. The kernel documentation for tcp_syncookies also says that with the default of 1, cookies are sent only when the SYN backlog overflows, and that it should not be used to help heavily loaded servers. The value our team read in the container above was also 1, and tcp_max_syn_backlog was 2048. These settings are mechanisms for dealing with SYN floods, and the mitigation listed in the advisory for this CVE is Red Hat’s option packet filter. So you cannot judge exposure to this flaw from the fact that SYN cookies are on.

A date attached to the word “safe”

So far we have matched three ledgers. The package ledger says which build is installed, the boot ledger says whether the server actually started with that kernel, and the infrastructure ledger says how many execution environments sit on the same kernel. The three ledgers are written by different people and updated on different cycles. That is why it is more accurate to write “safe” not as a full stop but as a dated record. A sentence like “as of Nov 2, this server was restarted at this time and is running this package build” can be reread and corrected later when the tracker status changes, but “patch complete” written without a date loses its basis the moment an advisory is updated. In this incident the cells in the trackers were still moving (Ubuntu’s “in progress” and Red Hat’s “in progress” are examples), and your server’s cells move along with them. If the question “has the patch come out?” makes you look for the boot record of the same date as well, then the question this article posed has done its job.

#CVE-2026-98432#Linux kernel#use-after-free#TCP#nftables

Sources