Sanniv Khare 7 min read

CVE-2026-98432 in Go: finding the hidden MPTCP listeners

CVE-2026-98432 in Go: finding the hidden MPTCP listeners

If you run Go services built with Go 1.24 or later on Linux, each net.Listen("tcp", ...) also opens an MPTCP listening socket, and plain ss -tln does not show it. Run ss -tlnM instead: any line starting with mptcp is a listener that meets the precondition of the public CVE-2026-98432 root exploit. Setting GODEBUG=multipathtcp=0 (or godebug multipathtcp=0 in go.mod) removes that socket. The real fix is a kernel at or past 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56 or 7.2.10.

We tested all of this with Go 1.24.7 on Linux 6.18.44 in a Linux container on 2 Nov 2026, with iproute2 6.1.0 for ss.

Why we did this: the backend for our apps runs a few Go services next to Laravel and Node on our Ubuntu KVM, and Ubuntu had not confirmed a fixed kernel when the Go Security Team’s notice went out on 29 Oct. Before changing any systemd unit we wanted to see exactly what Go opens, and how to prove a change worked.

What CVE-2026-98432 is, briefly

CVE-2026-98432 is a use-after-free in the Linux IPv4 TCP code, published on 15 Oct 2026. The kernel CNA scores it 9.8; Red Hat scores it 8.1 because it rates the attack complexity as high. It affects kernels from 4.4 up to the fixed releases listed above.

The demonstrated exploit, which gives unauthenticated root, needs an MPTCP listener on the target. The bug itself is not MPTCP-specific, and nobody has said whether root is reachable without one. So removing MPTCP listeners takes away the demonstrated precondition, not the bug.

Distro status as reported on 30 Oct: Debian 12 and 13 had fixed kernels; RHEL 8, 9 and 10 were listed as affected with no fixed packages; Ubuntu had no confirmed fix on its tracker. Check the Ubuntu CVE page for your release on the day you read this.

Why Go matters here

Since Go 1.24, Go enables MPTCP by default for listen sockets on Linux. The Go team’s notice of 29 Oct says it plainly and calls per-program opt-outs a “fragile mitigation”: one forgotten listener is enough. It also says the team considers it premature to change Go’s default because of this bug.

To see it, we built a 25-line server that listens on 127.0.0.1:8081 with net.Listen and nothing else, with go 1.24 in go.mod:

ln, err := net.Listen("tcp", "127.0.0.1:8081")
if err != nil {
    panic(err)
}
Terminal: ss -tlnM shows a tcp and an mptcp listener on port 8081 by default, and only tcp with GODEBUG=multipathtcp=0

Real output from the container. The default build gets an extra mptcp socket; the environment variable removes it.

Two things stood out:

  • ss -tln 'sport = :8081' printed exactly one line in both runs. Without -M (--mptcp) you cannot tell the difference, so a quick port audit with the usual flags misses this completely.
  • The same listener shows up twice with -M: once as tcp and once as mptcp. Clients that only speak TCP still connect normally, which is why nothing looks broken.

Three ways to turn it off, tested

All three removed the mptcp line from ss -tlnM for our test server.

1. Environment variable, no rebuild. Good for a running fleet:

GODEBUG=multipathtcp=0 ./server

In a systemd unit that is one line under [Service], followed by systemctl daemon-reload and a restart of the service. If the unit already sets GODEBUG, add the setting to the existing value with a comma; a second Environment=GODEBUG= line replaces the first.

[Service]
Environment=GODEBUG=multipathtcp=0

2. go.mod, at build time. Survives a unit file being rewritten by a deploy tool:

module example.com/server

go 1.24

godebug multipathtcp=0

3. The go line itself. A module that still says go 1.23 gets the old default even when built with a 1.24 toolchain, because Go picks GODEBUG defaults from the module’s language version. Our go 1.23 build of the same code had no MPTCP listener.

There is also a per-socket switch, net.ListenConfig.SetMultipathTCP(false), which overrides GODEBUG. That cuts both ways: if a library calls SetMultipathTCP(true), your environment variable will not stop it, so check ss -tlnM after the change rather than trusting the config.

Audit the binaries without running them

Go embeds the effective defaults in each binary. go version -m prints them, so you can check what is deployed without starting anything:

go version -m ./server | grep DefaultGODEBUG

Our godebug build printed DefaultGODEBUG=multipathtcp=0. The go 1.23 build printed a longer list that also contained multipathtcp=0. The plain go 1.24 build printed no DefaultGODEBUG line at all, which means it uses the 1.24 defaults, MPTCP on.

A read-only check script

We put the three checks in one script: kernel version against the upstream fix for its series, the net.mptcp.enabled sysctl, and every MPTCP listener with its process, Go version and GODEBUG settings. It changes nothing.

#!/usr/bin/env bash
# mptcp-check.sh: CVE-2026-98432 exposure check. Read-only. Run as root so ss can name processes.
set -u
fixed="5.10.272 5.15.223 6.1.190 6.6.159 6.12.113 6.18.56 7.2.10"
k=$(uname -r | cut -d- -f1); series=${k%.*}
echo "kernel: $(uname -r)"
for f in $fixed; do
  if [ "${f%.*}" = "$series" ]; then
    if [ "$(printf '%s\n%s\n' "$f" "$k" | sort -V | head -1)" = "$f" ]; then
      echo "  upstream $series fix is $f: this version is at or past it"
    else
      echo "  upstream $series fix is $f: this version is OLDER (check your distro's backport notice)"
    fi
  fi
done
echo "net.mptcp.enabled = $(sysctl -n net.mptcp.enabled 2>/dev/null || echo 'n/a (no MPTCP in kernel)')"
echo "MPTCP listeners:"
ss -Hltnp -M 2>/dev/null | awk '$1=="mptcp"' | sed 's/^/  /'
for pid in $(ss -Hltnp -M 2>/dev/null | awk '$1=="mptcp"' | grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u); do
  exe=$(readlink -f /proc/$pid/exe)
  gv=$(go version -m "$exe" 2>/dev/null | head -1)
  dg=$(go version -m "$exe" 2>/dev/null | grep -o 'DefaultGODEBUG=[^ ]*')
  echo "  pid $pid $exe ${gv:+[$gv]} ${dg:-}"
  tr '\0' '\n' < /proc/$pid/environ 2>/dev/null | grep '^GODEBUG=' | sed 's/^/    env /'
done
Terminal: the check script reports kernel 6.18.44 older than the 6.18.56 fix, MPTCP enabled, and two MPTCP listeners

The script run in the same container. The second listener belongs to the container’s own management process, not to our test.

The run made a useful point by accident: the container had an MPTCP listener we had not started. On a real server, expect the same from Go-based tools you installed rather than wrote (agents, exporters, proxies). A distro kernel version number like Ubuntu’s 6.8.0-xx will not match the upstream list either, so treat an “OLDER” result as “go and read your distro’s notice”, not as proof of exposure.

The blunt option: the sysctl

net.mptcp.enabled defaults to 1. Setting it to 0 stops new MPTCP sockets from being created for every process at once, which is less fragile than chasing each service. We did not toggle it in this container, so we have no output to show for it; the semantics here come from the kernel documentation and the advisory write-ups. If nothing you run depends on MPTCP (most web backends don’t), it is the simplest stopgap until a fixed kernel lands, and it still leaves the underlying TCP bug in place.

What to do today

  1. Run ss -tlnM (or the script above) on every Linux box with Go services.
  2. For each mptcp listener, add GODEBUG=multipathtcp=0 or the godebug line, restart, and run ss -tlnM again.
  3. Consider net.mptcp.enabled=0 if you cannot account for every listener.
  4. Install the fixed kernel as soon as your distro ships it and reboot; that is the actual fix.

Sources: the Go Security Team notice on oss-security (29 Oct 2026), lilting.ch’s CVE-2026-98432 summary with the fixed-version list (updated 30 Oct 2026), and Go issue 82717 for the discussion on Go’s default.

Need this built? Let’s talk.

We build and run apps for Mac, TV, mobile and the web, and write about what we learn.