]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ipv6: Implement limits on extension header parsing
authorDaniel Borkmann <daniel@iogearbox.net>
Wed, 29 Apr 2026 15:46:48 +0000 (17:46 +0200)
committerJakub Kicinski <kuba@kernel.org>
Fri, 1 May 2026 00:21:45 +0000 (17:21 -0700)
ipv6_{skip_exthdr,find_hdr}() and ip6_{tnl_parse_tlv_enc_lim,
protocol_deliver_rcu}() iterate over IPv6 extension headers until they
find a non-extension-header protocol or run out of packet data. The
loops have no iteration counter, relying solely on the packet length
to bound them. For a crafted packet with 8-byte extension headers
filling a 64KB jumbogram, this means a worst case of up to ~8k
iterations with a skb_header_pointer call each. ipv6_skip_exthdr(),
for example, is used where it parses the inner quoted packet inside
an incoming ICMPv6 error:

  - icmpv6_rcv
    - checksum validation
    - case ICMPV6_DEST_UNREACH
      - icmpv6_notify
        - pskb_may_pull()       <- pull inner IPv6 header
        - ipv6_skip_exthdr()    <- iterates here
        - pskb_may_pull()
        - ipprot->err_handler() <- sk lookup

The per-iteration cost of ipv6_skip_exthdr itself is generally
light, but skb_header_pointer becomes more costly on reassembled
packets: the first ~1232 bytes of the inner packet are in the skb's
linear area, but the remaining ~63KB are in the frag_list where
skb_copy_bits is needed to read data.

Initially, the idea was to add a configurable limit via a new
sysctl knob with default 8, in line with knobs from commit
47d3d7ac656a ("ipv6: Implement limits on Hop-by-Hop and Destination
options"), but two reasons eventually argued against it:

- It adds to UAPI that needs to be maintained forever, and
  upcoming work is restricting extension header ordering anyway,
  leaving little reason for another sysctl knob
- exthdrs_core.c is always built-in even when CONFIG_IPV6=n,
  where struct net has no .ipv6 member, so the read site would
  need an ifdef'd fallback to a constant anyway

Therefore, just use a constant (IP6_MAX_EXT_HDRS_CNT). All four
extension header walking functions are now bound by this limit.

Note that the check in ip6_protocol_deliver_rcu() happens right
before the goto resubmit, such that we don't have to have a test
for ipv6_ext_hdr() in the fast-path.

There's an ongoing IETF draft-iurman-6man-eh-occurrences to enforce
IPv6 extension headers ordering and occurrence. The latter also
discusses security implications. As per RFC8200 section 4.1, the
occurrence rules for extension headers provide a practical upper
bound which is 8. In order to be conservative, let's define
IP6_MAX_EXT_HDRS_CNT as 12 to leave enough room for quirky setups.
In the unlikely event that this is still not enough, then we might
need to reconsider a sysctl.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Eric Dumazet <edumazet@google.com>
Reviewed-by: Justin Iurman <justin.iurman@gmail.com>
Link: https://patch.msgid.link/20260429154648.809751-1-daniel@iogearbox.net
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
include/net/dropreason-core.h
include/net/ipv6.h
net/ipv6/exthdrs_core.c
net/ipv6/ip6_input.c
net/ipv6/ip6_tunnel.c

index e0ca3904ff8e0cffb18b6e3f2a604cc97505cff0..2f312d1f67d69869988854f9f76648677b6622e4 100644 (file)
@@ -99,6 +99,7 @@
        FN(FRAG_TOO_FAR)                \
        FN(TCP_MINTTL)                  \
        FN(IPV6_BAD_EXTHDR)             \
+       FN(IPV6_TOO_MANY_EXTHDRS)       \
        FN(IPV6_NDISC_FRAG)             \
        FN(IPV6_NDISC_HOP_LIMIT)        \
        FN(IPV6_NDISC_BAD_CODE)         \
@@ -494,6 +495,11 @@ enum skb_drop_reason {
        SKB_DROP_REASON_TCP_MINTTL,
        /** @SKB_DROP_REASON_IPV6_BAD_EXTHDR: Bad IPv6 extension header. */
        SKB_DROP_REASON_IPV6_BAD_EXTHDR,
+       /**
+        * @SKB_DROP_REASON_IPV6_TOO_MANY_EXTHDRS: Number of IPv6 extension
+        * headers in the packet exceeds IP6_MAX_EXT_HDRS_CNT.
+        */
+       SKB_DROP_REASON_IPV6_TOO_MANY_EXTHDRS,
        /** @SKB_DROP_REASON_IPV6_NDISC_FRAG: invalid frag (suppress_frag_ndisc). */
        SKB_DROP_REASON_IPV6_NDISC_FRAG,
        /** @SKB_DROP_REASON_IPV6_NDISC_HOP_LIMIT: invalid hop limit. */
index d042afe7a24564a5a27ed16515500f17f0e39977..1dec81faff282f4b3278cb59979974d8fc4c0729 100644 (file)
@@ -90,6 +90,9 @@ struct ip_tunnel_info;
 #define IP6_DEFAULT_MAX_DST_OPTS_LEN    INT_MAX /* No limit */
 #define IP6_DEFAULT_MAX_HBH_OPTS_LEN    INT_MAX /* No limit */
 
+/* Hard limit on traversed IPv6 extension headers */
+#define IP6_MAX_EXT_HDRS_CNT            12
+
 /*
  *     Addr type
  *     
index 49e31e4ae7b7f661d555e73e0515983e9584eec4..9d06d487e8b103ef89fb723d746a7e7be4473758 100644 (file)
@@ -73,6 +73,7 @@ int ipv6_skip_exthdr(const struct sk_buff *skb, int start, u8 *nexthdrp,
                     __be16 *frag_offp)
 {
        u8 nexthdr = *nexthdrp;
+       int exthdr_cnt = 0;
 
        *frag_offp = 0;
 
@@ -82,6 +83,8 @@ int ipv6_skip_exthdr(const struct sk_buff *skb, int start, u8 *nexthdrp,
 
                if (nexthdr == NEXTHDR_NONE)
                        return -1;
+               if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
+                       return -1;
                hp = skb_header_pointer(skb, start, sizeof(_hdr), &_hdr);
                if (!hp)
                        return -1;
@@ -190,6 +193,7 @@ int ipv6_find_hdr(const struct sk_buff *skb, unsigned int *offset,
 {
        unsigned int start = skb_network_offset(skb) + sizeof(struct ipv6hdr);
        u8 nexthdr = ipv6_hdr(skb)->nexthdr;
+       int exthdr_cnt = 0;
        bool found;
 
        if (fragoff)
@@ -216,6 +220,9 @@ int ipv6_find_hdr(const struct sk_buff *skb, unsigned int *offset,
                        return -ENOENT;
                }
 
+               if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
+                       return -EBADMSG;
+
                hp = skb_header_pointer(skb, start, sizeof(_hdr), &_hdr);
                if (!hp)
                        return -EBADMSG;
index 967b07aeb6831e2e1d3f1526c5be04da8a18b68b..8972863c93ee525eba54a85e780bdc1383470fa3 100644 (file)
@@ -403,6 +403,7 @@ INDIRECT_CALLABLE_DECLARE(int tcp_v6_rcv(struct sk_buff *));
 void ip6_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int nexthdr,
                              bool have_final)
 {
+       int exthdr_cnt = IP6CB(skb)->flags & IP6SKB_HOPBYHOP ? 1 : 0;
        const struct inet6_protocol *ipprot;
        struct inet6_dev *idev;
        unsigned int nhoff;
@@ -487,6 +488,10 @@ resubmit_final:
                                nexthdr = ret;
                                goto resubmit_final;
                        } else {
+                               if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT)) {
+                                       SKB_DR_SET(reason, IPV6_TOO_MANY_EXTHDRS);
+                                       goto discard;
+                               }
                                goto resubmit;
                        }
                } else if (ret == 0) {
index c468c83af0f20e72b2f40315c3dcd6aef3de9e60..9d1037ac082f6aad36c152ffdbe0f30237b65fc3 100644 (file)
@@ -399,11 +399,15 @@ __u16 ip6_tnl_parse_tlv_enc_lim(struct sk_buff *skb, __u8 *raw)
        unsigned int nhoff = raw - skb->data;
        unsigned int off = nhoff + sizeof(*ipv6h);
        u8 nexthdr = ipv6h->nexthdr;
+       int exthdr_cnt = 0;
 
        while (ipv6_ext_hdr(nexthdr) && nexthdr != NEXTHDR_NONE) {
                struct ipv6_opt_hdr *hdr;
                u16 optlen;
 
+               if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
+                       break;
+
                if (!pskb_may_pull(skb, off + sizeof(*hdr)))
                        break;