net/ipv6/exthdrs.c | 4 ++++ 1 file changed, 4 insertions(+)
ipv6_rpl_srh_rcv() dereferences idev from __in6_dev_get() without a
NULL check when reading idev->cnf.rpl_seg_enabled.
When the device's MTU drops below IPV6_MIN_MTU, addrconf_ifdown()
clears dev->ip6_ptr through RCU_INIT_POINTER(), which is immediately
visible to concurrent readers. A packet that already passed the idev
check in ip6_rcv_core() can race with this and hit a NULL pointer
dereference.
Reproduced by flooding traffic while rapidly flapping the receiving
interface's MTU between 1500 and 1200:
BUG: KASAN: null-ptr-deref in ipv6_rpl_srh_rcv+0xae/0x1050
Read of size 4 at addr 00000000000006b4 by task ping6/386
CPU: 0 UID: 0 PID: 386 Comm: ping6 Not tainted 7.1.0-rc3 #114 PREEMPT(full)
Call Trace:
<IRQ>
kasan_report+0xc6/0x100
ipv6_rpl_srh_rcv+0xae/0x1050
ip6_protocol_deliver_rcu+0x754/0x9a0
ip6_input_finish+0xa3/0x1b0
ip6_input+0xdc/0x490
ipv6_rcv+0x338/0x460
__netif_receive_skb_one_core+0xd1/0x130
process_backlog+0x2c7/0x9f0
__napi_poll.constprop.0+0x51/0x270
net_rx_action+0x322/0x730
handle_softirqs+0x119/0x640
do_softirq+0xae/0xe0
</IRQ>
Add a NULL check for idev after __in6_dev_get(), dropping the skb
with SKB_DROP_REASON_IPV6DISABLED when the device has no IPv6
configuration.
Fixes: 8610c7c6e3bd ("net: ipv6: add support for rpl sr exthdr")
Cc: stable@vger.kernel.org
Signed-off-by: Andrea Mayer <andrea.mayer@uniroma2.it>
---
v2:
- use SKB_DROP_REASON_IPV6DISABLED as drop reason (Eric Dumazet)
v1: https://lore.kernel.org/netdev/20260428224816.11223-1-andrea.mayer@uniroma2.it/
---
net/ipv6/exthdrs.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
index 03cbce842c1a..a4af6e63349c 100644
--- a/net/ipv6/exthdrs.c
+++ b/net/ipv6/exthdrs.c
@@ -499,6 +499,10 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
u32 r;
idev = __in6_dev_get(skb->dev);
+ if (!idev) {
+ kfree_skb_reason(skb, SKB_DROP_REASON_IPV6DISABLED);
+ return -1;
+ }
accept_rpl_seg = min(READ_ONCE(net->ipv6.devconf_all->rpl_seg_enabled),
READ_ONCE(idev->cnf.rpl_seg_enabled));
--
2.43.0
On 5/18/26 8:06 AM, Andrea Mayer wrote:
> diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
> index 03cbce842c1a..a4af6e63349c 100644
> --- a/net/ipv6/exthdrs.c
> +++ b/net/ipv6/exthdrs.c
> @@ -499,6 +499,10 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
> u32 r;
>
> idev = __in6_dev_get(skb->dev);
> + if (!idev) {
> + kfree_skb_reason(skb, SKB_DROP_REASON_IPV6DISABLED);
> + return -1;
> + }
>
> accept_rpl_seg = min(READ_ONCE(net->ipv6.devconf_all->rpl_seg_enabled),
> READ_ONCE(idev->cnf.rpl_seg_enabled));
ipv6_rpl_srh_rcv and ipv6_srh_rcv are both called by ipv6_rthdr_rcv and
both of these functions check idev. Moving the check to ipv6_rthdr_rcv
which already has an idev lookup would simplifying both paths -- and set
the drop code reason the same.
On Mon, 18 May 2026 08:18:56 -0600
David Ahern <dsahern@kernel.org> wrote:
> On 5/18/26 8:06 AM, Andrea Mayer wrote:
> > diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
> > index 03cbce842c1a..a4af6e63349c 100644
> > --- a/net/ipv6/exthdrs.c
> > +++ b/net/ipv6/exthdrs.c
> > @@ -499,6 +499,10 @@ static int ipv6_rpl_srh_rcv(struct sk_buff *skb)
> > u32 r;
> >
> > idev = __in6_dev_get(skb->dev);
> > + if (!idev) {
> > + kfree_skb_reason(skb, SKB_DROP_REASON_IPV6DISABLED);
> > + return -1;
> > + }
> >
> > accept_rpl_seg = min(READ_ONCE(net->ipv6.devconf_all->rpl_seg_enabled),
> > READ_ONCE(idev->cnf.rpl_seg_enabled));
>
> ipv6_rpl_srh_rcv and ipv6_srh_rcv are both called by ipv6_rthdr_rcv and
> both of these functions check idev. Moving the check to ipv6_rthdr_rcv
> which already has an idev lookup would simplifying both paths -- and set
> the drop code reason the same.
Hi David,
thanks for the review. I went through the code to plan v3, and I'd like to
share a couple of points before sending it, in case I'm missing something.
ipv6_rthdr_rcv() seems to already tolerate idev == NULL. The per-device
accept_source_route read is wrapped in "if (idev)", and all
__IP6_INC_STATS() calls go through _DEVINC(), which is NULL safe.
The code after the switch:
[...]
switch (hdr->type) {
case IPV6_SRCRT_TYPE_4:
return ipv6_srh_rcv(skb);
case IPV6_SRCRT_TYPE_3:
return ipv6_rpl_srh_rcv(skb);
default:
break;
}
[...]
falls back to the "default: break;" path, which only uses idev through
those macros. For this reason, I think an "if (!idev) drop" at the top
would change the behavior of the default path. So if we want to put the
check on idev in this function, the check would need to go inside the two
switch cases.
Both the callees in the switch need idev to read the per-device sysctl
(idev->cnf.seg6_enabled, idev->cnf.rpl_seg_enabled). To remove their own
__in6_dev_get() they would have to receive idev from the caller.
Two possible shapes for v3, both pass idev to the callees and remove their
own __in6_dev_get() and NULL check:
(a) Check inside the two switch cases with goto to a "disabled:" label at
the end of the function (same style as the existing "unknown_rh:"):
switch (hdr->type) {
case IPV6_SRCRT_TYPE_4:
if (!idev)
goto disabled;
return ipv6_srh_rcv(skb, idev);
case IPV6_SRCRT_TYPE_3:
if (!idev)
goto disabled;
return ipv6_rpl_srh_rcv(skb, idev);
default:
break;
}
[...]
disabled:
kfree_skb_reason(skb, SKB_DROP_REASON_IPV6DISABLED);
return -1;
(b) Single check before the switch, only for the two types that need
idev:
if (!idev && (hdr->type == IPV6_SRCRT_TYPE_4 ||
hdr->type == IPV6_SRCRT_TYPE_3)) {
kfree_skb_reason(skb, SKB_DROP_REASON_IPV6DISABLED);
return -1;
}
/* switch unchanged, idev passed to the callees */
Both change the signatures of ipv6_srh_rcv() and ipv6_rpl_srh_rcv().
Any preference, or a different approach in mind?
Thanks,
Andrea
© 2016 - 2026 Red Hat, Inc.