November 2, 2026
A Critical Linux Bug Dropped. Here’s How I Checked My Server.
CVE-2026–98432 lets an attacker get remote root through ordinary TCP connection setup. I went through my own Linux box line by line to find…

By Gabriel Babajide Shomorin
10 min read
CVE-2026–98432 lets an attacker get remote root through ordinary TCP connection setup. I went through my own Linux box line by line to find out how exposed it was, and here is what I found and how I locked it down.
On a quiet Sunday afternoon, this was going around my Twitter feed:
"Hard to overstate how big of a deal this is. Any unpatched Linux server with even a single TCP port exposed to the internet and MPTCP active (default for many Go web servers) is vulnerable to complete takeover."
I have a small Linux server on the public internet. It runs a few side projects, has SSH open, and sits behind a reverse proxy written in Go. My first thought was "that's me." My second was "I don't actually know whether that's me."
This article covers how I found out and what I changed. You can follow along on your own server in about fifteen minutes.
Part 1: What the bug is
The vulnerability is CVE-2026–98432. The Linux kernel CVE team rated it 9.8 out of 10.
-What it is: a use-after-free in the kernel's IPv4 TCP connection-setup code (the path through ip_options_build()). Memory gets freed and then used again. With careful timing, an attacker can put their own data in that freed memory.
-Who can trigger it: any remote client. No credentials are needed, only the ability to open a TCP connection to your machine.
-What was demonstrated: researchers got remote root code execution against a server listening with MPTCP (Multipath TCP). Their exploit first leaked a kernel address to get around KASLR, then overwrote a callback pointer. It reportedly took about 53 minutes from start to root.
- How far back it goes: some write-ups say kernels back to 4.4 are affected. Check your distribution's advisory rather than relying on that number.
Why MPTCP matters
MPTCP lets one connection use several network paths at once, such as Wi-Fi and cellular. It's useful, but most server owners never knowingly turned it on.
The catch is that recent Go releases ask for MPTCP on listening sockets by default. If you run a web server, reverse proxy, or API written in Go (Caddy, Traefik, many custom services), it may be accepting MPTCP connections without you knowing.
The part that's easy to miss
The bug is in TCP itself, not only in MPTCP. MPTCP is the path that was proven exploitable. The faulty code can still be reached with ordinary TCP traffic. No public exploit has shown remote code execution that way yet, but turning off MPTCP shrinks your exposure without fixing the bug.
Only a patched kernel that is actually running fixes it.
Part 2: Checking my server
I logged in and asked the server four questions.
Question 1: Which kernel is actually running?
uname -r
apt list - installed 2>/dev/null | grep -E '^linux-image'uname -r
apt list - installed 2>/dev/null | grep -E '^linux-image'This trips anyone up: the kernel installed on disk and the kernel running in memory are different things. Running `` and seeing a new linux-image package doesn't protect you until you reboot into it. uname -r shows what's running now.
My result:
7.0.0–38-generic
linux-image-generic … 7.0.0–38.38 [installed,upgradable to: 7.0.0–42.42]7.0.0–38-generic
linux-image-generic … 7.0.0–38.38 [installed,upgradable to: 7.0.0–42.42]Next I checked my distribution's CVE tracker. I'm on Ubuntu 26.04 LTS, and the Ubuntu page for CVE-2026–98432 said:
Vulnerable — work in progress in 7.0.0–43.43
So my kernel was vulnerable. The pending -42 upgrade wouldn't fix it either, because the fix was still being prepared for -43.
Checking that an upgrade exists isn't enough. Check that the upgrade contains the fix.
The upstream stable kernels with the fix were 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10, 7.3 and later. Distro kernels backport fixes under their own version numbers, though, so use your distro's advisory as the source of truth, not upstream version numbers.
Question 2: Is MPTCP turned on?
sysctl net.mptcp.enabled
net.mptcp.enabled = 1sysctl net.mptcp.enabled
net.mptcp.enabled = 1It was on. That only means the kernel allows MPTCP. Something still has to be listening with it.
Question 3: Is anything listening with MPTCP?
sudo ss -M -ltnp # MPTCP listeners
sudo ss -ltnp # all TCP listenerssudo ss -M -ltnp # MPTCP listeners
sudo ss -ltnp # all TCP listenersTo read the output, look at the Netid column. MPTCP sockets show up as mptcp. Mine all showed tcp:
Netid State Local Address:Port Process
tcp LISTEN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 0.0.0.0:22 sshd
tcp LISTEN [::]:22 sshdNetid State Local Address:Port Process
tcp LISTEN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 0.0.0.0:22 sshd
tcp LISTEN [::]:22 sshdThere were no MPTCP listeners. The only service reachable from the internet was SSH on port 22. OpenSSH is written in C and doesn't use MPTCP.
There was a surprise, though. My Go reverse proxy wasn't listening on 80 or 443 at all. The stack happened to be down. When I started it again, it would most likely open MPTCP listeners and expose exactly the setup the exploit was built for. At that moment I was safe from the published exploit only by luck.
Question 4: Is the other trigger path turned on?
The Ubuntu advisory also lists request-socket migration as a way to reach the bug:
sysctl net.ipv4.tcp_migrate_req
net.ipv4.tcp_migrate_req = 0sysctl net.ipv4.tcp_migrate_req
net.ipv4.tcp_migrate_req = 0It was off, which is the default. If yours is 1, find out what turned it on.
Where that left me
https://datawrapper.dwcdn.net/YrhC1/1/
In short: vulnerable, but the proven attack had no way in yet. My plan was to keep that door closed before the stack came back, make the remaining door smaller, and patch the moment a fix shipped.
Part 3: Locking it down, step by step
Step 1: Turn off MPTCP system-wide
This closes the demonstrated exploit path, whatever starts listening later:
echo 'net.mptcp.enabled=0' | sudo tee /etc/sysctl.d/99-no-mptcp.conf
sudo sysctl - system
sysctl net.mptcp.enabled # should print 0echo 'net.mptcp.enabled=0' | sudo tee /etc/sysctl.d/99-no-mptcp.conf
sudo sysctl - system
sysctl net.mptcp.enabled # should print 0The file in /etc/sysctl.d/ keeps the setting after a reboot. Unless you knowingly use multipath networking, you won't notice any difference.
Step 2: Tell your Go services not to ask for MPTCP
This is a second layer that keeps working if someone turns the sysctl back on. Go supports a per-process switch:
GODEBUG=multipathtcp=0GODEBUG=multipathtcp=0In Docker Compose, add it to the Go service:
services:
caddy:
environment:
- GODEBUG=multipathtcp=0services:
caddy:
environment:
- GODEBUG=multipathtcp=0For a systemd service, add Environment=GODEBUG=multipathtcp=0 to the unit.
Note: This is a stopgap. It removes one attack path and leaves the underlying bug in place.
Step 3: Make the remaining TCP exposure smaller
The bug can be reached with plain TCP, so every port open to the whole internet is a way in. Ports 80 and 443 must stay public for a website. SSH doesn't need to.
The plan is to allow SSH only from my own IP address. That raises an obvious question: what is my IP?
Step 3a: Get your IP from the server's point of view
My first idea was to Google "what's my IP" and copy whatever came back. That's the wrong number to trust. VPNs, proxies, corporate networks, and carrier-grade NAT can all make a website see a different address from the one your SSH connection comes from.
If you allow the wrong address and then delete the open rule, you're locked out.
I almost did exactly that. The address I had in mind was not the one my server was seeing.
The reliable way is to ask the server who it's talking to. From inside your current SSH session, run:
echo $SSH_CLIENTecho $SSH_CLIENTThe output has three fields:
203.0.113.42 28381 22203.0.113.42 28381 22Field | Meaning |
203.0.113.42 | Your IP as the server sees it. This is the one to use.
28381 | The port on your side of the connection (random, ignore it) |
22 | The server's SSH port |
If you're on sudo or su and the variable is empty, these show the same information:
who am i # shows your session and source IP in parentheses
ss -tnp | grep sshd # lists active SSH connections and their peer addresseswho am i # shows your session and source IP in parentheses
ss -tnp | grep sshd # lists active SSH connections and their peer addressesStep 3b: Add the new rule before removing the old one
Order matters here. Add the narrow rule first, test it, and remove the open rule last:
# 1. Allow your IP (the one from $SSH_CLIENT)
sudo ufw allow from 203.0.113.42 to any port 22 proto tcp
# 2. In a SECOND terminal, open a fresh SSH session and make sure it works
ssh user@your-server
# 3. Only then remove the open-to-everyone rule (this removes both IPv4 and IPv6)
sudo ufw delete allow 22/tcp
# 4. Check the result
sudo ufw status numbered# 1. Allow your IP (the one from $SSH_CLIENT)
sudo ufw allow from 203.0.113.42 to any port 22 proto tcp
# 2. In a SECOND terminal, open a fresh SSH session and make sure it works
ssh user@your-server
# 3. Only then remove the open-to-everyone rule (this removes both IPv4 and IPv6)
sudo ufw delete allow 22/tcp
# 4. Check the result
sudo ufw status numberedAdding the specific rule does nothing on its own. While the generic 22/tcp ALLOW Anywhere rule exists, everyone still gets in. Step 3 is where the lockdown actually takes effect.
What a correct result looks like:
[ 1] 80/tcp ALLOW IN Anywhere
[ 2] 443/tcp ALLOW IN Anywhere
[ 3] 22/tcp ALLOW IN 203.0.113.42
[ 4] 80/tcp (v6) ALLOW IN Anywhere (v6)
[ 5] 443/tcp (v6) ALLOW IN Anywhere (v6)[ 1] 80/tcp ALLOW IN Anywhere
[ 2] 443/tcp ALLOW IN Anywhere
[ 3] 22/tcp ALLOW IN 203.0.113.42
[ 4] 80/tcp (v6) ALLOW IN Anywhere (v6)
[ 5] 443/tcp (v6) ALLOW IN Anywhere (v6)Port 22 appears once, from your address only, and there's no 22/tcp (v6) Anywhere line left behind.
Your current session keeps working through all of this, because ufw doesn't cut off connections that are already established. That's exactly why you test with a new session in step 2.
Step 3c: Watch out for a changing IP
A few hours later I logged in again and the address on the rule no longer matched. My home IP had changed. Looking back at my login history (the Last login: … from … line), it had been changing all along within the same ISP block.
If your IP is dynamic, a single-IP rule will eventually lock you out. Your options:
-
Allow your ISP's range, for example
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp. It's quick and much smaller than "Anywhere," but not as tight. -
Use a VPN such as Tailscale or WireGuard. Allow port 22 only on the VPN interface and stop exposing it publicly at all. This is the proper fix.
-
Whatever you choose, bookmark your provider's web console. It's your way back in if a firewall change goes wrong.
⚠️ Always keep a working session open while you change firewall or SSH settings. Locking yourself out of a remote server is the one mistake here that's hard to undo.
Step 3d: Docker skips ufw
If you run containers, there's one more catch. Ports published by Docker (ports: in Compose) go around ufw entirely, because Docker writes its own iptables rules. A line like 6379:6379 for Redis makes it reachable from the internet no matter what ufw status says.
Check with docker ps. In the PORTS column, 6379/tcp means internal only, which is fine. 0.0.0.0:6379->6379/tcp means published to the world. Leave internal services unpublished, or bind them to localhost (127.0.0.1:6379:6379).
Step 4: Fix the things you've been putting off
My check turned up other problems:
-I was logging in as root. The lockdown step from my original setup had never been done.
-
74 pending updates had piled up.
-
The login banner was already recommending fail2ban.
The fix:
# Apply everything pending (won't fix this CVE yet, but fixes others)
sudo apt update && sudo apt full-upgrade
# Brute-force protection
sudo apt install -y fail2ban
# In /etc/ssh/sshd_config - only after confirming a non-root sudo user can log in:
# PermitRootLogin no
# PasswordAuthentication no
sudo systemctl restart ssh
# Apply everything pending (won't fix this CVE yet, but fixes others)
sudo apt update && sudo apt full-upgrade
# Brute-force protection
sudo apt install -y fail2ban
# In /etc/ssh/sshd_config - only after confirming a non-root sudo user can log in:
# PermitRootLogin no
# PasswordAuthentication no
sudo systemctl restart sshStep 5: Patch, reboot, and check
This is the only step that actually fixes the bug.
-
Watch your distro's tracker Ubuntu, Debian, or Red Hat Product Security) until your release shows a released fix. Debian 12 and 13 had fixed packages by October 30. Ubuntu's were still in progress when I wrote this.
-
Upgrade and reboot:
sudo apt update && sudo apt full-upgrade
sudo rebootsudo apt update && sudo apt full-upgrade
sudo reboot- Check that the server came back healthy on the new kernel:
uname -r # must match the fixed version
systemctl - failed # nothing should have broken
journalctl -k -b -p warning # kernel warnings since bootuname -r # must match the fixed version
systemctl - failed # nothing should have broken
journalctl -k -b -p warning # kernel warnings since bootOnce uname -r shows the fixed version, you're patched. You can turn MPTCP back on if you actually need it. I'm leaving it off, since I never used it.
What my reboot looked like
I didn't wait for the CVE fix to reboot. The 74 pending updates included a newer kernel (not the fixed one yet) and several services that needrestart had held back. At the end of the upgrade, apt told me plainly:
Pending kernel upgrade!
Running kernel version:
7.0.0–38-generic
Diagnostics:
The currently running kernel version is not the expected kernel version 7.0.0–42-generic.Pending kernel upgrade!
Running kernel version:
7.0.0–38-generic
Diagnostics:
The currently running kernel version is not the expected kernel version 7.0.0–42-generic.That's the "installed isn't running" lesson in apt's own words.
Before rebooting, I checked that my mitigations would survive it:
cat /etc/sysctl.d/99-no-mptcp.conf # net.mptcp.enabled=0cat /etc/sysctl.d/99-no-mptcp.conf # net.mptcp.enabled=0ufw rules persist on their own. Then I rebooted and ran the checks once I was back in:
uname -r # 7.0.0–42-generic → new kernel loaded
sysctl net.mptcp.enabled # 0 → mitigation held
ufw status numbered # SSH restricted → firewall held
systemctl - failed # 0 loaded units → nothing broke
docker ps # all containers up → restart policies workeduname -r # 7.0.0–42-generic → new kernel loaded
sysctl net.mptcp.enabled # 0 → mitigation held
ufw status numbered # SSH restricted → firewall held
systemctl - failed # 0 loaded units → nothing broke
docker ps # all containers up → restart policies workedEverything came back by itself because every container had restart: unless-stopped. If yours don't, a reboot leaves them down until you start them again.
My server was mitigated and cleaner, but not yet patched. The kernel fix for my release was still on the way, so the last step is waiting for it, installing it, and rebooting once more.
The checklist
If you skipped to the end, this is the whole process:
uname -r # 1. Running kernel → compare with your distro's CVE page
sysctl net.mptcp.enabled # 2. MPTCP on?
sudo ss -M -ltnp # 3. Any "mptcp" listeners?
sysctl net.ipv4.tcp_migrate_req # 4. Should be 0
# Mitigate now
echo 'net.mptcp.enabled=0' | sudo tee /etc/sysctl.d/99-no-mptcp.conf && sudo sysctl - system
# + GODEBUG=multipathtcp=0 on Go services
# Restrict SSH (IP from the server's point of view!)
echo $SSH_CLIENT # first field = your IP
sudo ufw allow from <that-ip> to any port 22 proto tcp
# test a NEW session, then:
sudo ufw delete allow 22/tcp
# Fix for real
sudo apt update && sudo apt full-upgrade && sudo reboot
uname -r # confirm fixed versionuname -r # 1. Running kernel → compare with your distro's CVE page
sysctl net.mptcp.enabled # 2. MPTCP on?
sudo ss -M -ltnp # 3. Any "mptcp" listeners?
sysctl net.ipv4.tcp_migrate_req # 4. Should be 0
# Mitigate now
echo 'net.mptcp.enabled=0' | sudo tee /etc/sysctl.d/99-no-mptcp.conf && sudo sysctl - system
# + GODEBUG=multipathtcp=0 on Go services
# Restrict SSH (IP from the server's point of view!)
echo $SSH_CLIENT # first field = your IP
sudo ufw allow from <that-ip> to any port 22 proto tcp
# test a NEW session, then:
sudo ufw delete allow 22/tcp
# Fix for real
sudo apt update && sudo apt full-upgrade && sudo reboot
uname -r # confirm fixed versionWhat I took away from this
-
"Installed" isn't "running." Until you reboot, the patched kernel on disk does nothing.
-
"An update is available" isn't "a fix is available." Read the advisory for your exact release.
-
Know what your dependencies do by default. I never chose MPTCP. My Go reverse proxy would have chosen it for me.
-
Being safe by accident isn't a security plan. My server dodged the exploit only because one service happened to be down.
-
Ask the server who you are. Your IP according to a website and your IP according to your server can differ.
echo $SSH_CLIENTgives the one that matters for the firewall. -
Add before you remove, and test with a new session. Firewall changes should never leave you without a way in.
-
A scare is a good time to clean up. Root login, unpatched packages, and a world-open SSH port had all been sitting there. This bug got me to fix them.
At the time of writing there were no confirmed reports of this being exploited in the wild. The technique is public now, though, and attackers will be scanning for unpatched machines soon. Check your servers today.
Patch status is as of November 1, 2026. Check your distribution's advisory for current information.*
Sources
-
Hive Security — Check the Running Kernel, Not Just the Update Screen
-
BleepingComputer — Critical Linux kernel flaw allows remote root attacks on MPTCP servers
-
AlmaLinux — CVE-2026–98432: Patched kernels available in testing
-
SecurityOnline — Linux Kernel CVE-2026–98432 Flaw Enables Remote Root
Happy to receive questions, feedback and observations: