From: Dave Seddon <dave.seddon.ca@gmail.com>
To: netdev@vger.kernel.org
Cc: "David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Stanislav Fomichev <sdf@fomichev.me>,
Tom Herbert <tom@herbertland.com>,
Willem de Bruijn <willemdebruijn.kernel@gmail.com>,
linux-kernel@vger.kernel.org,
Dave Seddon <dave.seddon.ca@gmail.com>
Subject: [PATCH net-next v1 09/11] net: flow_dissector: bound fast-path tunnel recursion
Date: Wed, 15 Jul 2026 17:43:55 -0700 [thread overview]
Message-ID: <20260716004357.3652679-10-dave.seddon.ca@gmail.com> (raw)
In-Reply-To: <20260716004357.3652679-1-dave.seddon.ca@gmail.com>
The opt-in fast path descends into IP-in-IP and GRE tunnels by
recursion: flow_dissect_fast_ipv4()/ipv6() call
flow_dissect_fast_ipip_inner()/gre_inner(), which parse the inner IP
header and call back into flow_dissect_fast_ipv4()/ipv6(). Unlike the
slow path, which caps total header descents at MAX_FLOW_DISSECT_HDRS
via skb_flow_dissect_allowed(), the fast helpers had no such bound.
A crafted frame with a deeply nested tunnel chain
(IP-in-IP-in-IP-in-..., or GRE) therefore drives one C stack frame per
~20 bytes of linear header. hlen is skb_headlen(), so a large,
GRO-coalesced or locally crafted skb yields hundreds to thousands of
levels and can exhaust the kernel stack. The inner helpers also do
work after the recursive call (they stamp FLOW_DIS_ENCAPSULATION), so
the frames are not tail-call eliminable. It is additionally an output
divergence: past 15 headers the slow path stops and reports the
15th-level keys, while the fast path would recurse to the innermost,
breaking the byte-identical contract.
Thread a num_hdrs counter through the tunnel-reachable helpers and,
at each tunnel descent, mirror skb_flow_dissect_allowed(): once the
count exceeds MAX_FLOW_DISSECT_HDRS the helper returns false and the
dissect falls back to the slow path (which caps at the same limit).
For chains within the limit both paths descend fully and agree; beyond
it the fast path defers to the slow path's result, so the output stays
identical and the recursion is bounded.
The initial count at each fast-path entry is set at or above the
number of headers the slow path has already consumed (outer VLAN tags,
PPPoE, the outer IP), so the fast path never descends past the point
the slow path would have capped.
The added KUnit equivalence test's deep-nest case exercises IPIP and
GRE chains across the cap boundary; with the bound removed it fails at
16 levels, confirming it guards this fix.
Assisted-by: Claude:claude-fable-5 sparse smatch
Signed-off-by: Dave Seddon <dave.seddon.ca@gmail.com>
---
net/core/flow_dissector.c | 61 ++++++++++++++++++++++++---------------
1 file changed, 37 insertions(+), 24 deletions(-)
diff --git a/net/core/flow_dissector.c b/net/core/flow_dissector.c
index 2a4b9b2af834..49b256ce5c1e 100644
--- a/net/core/flow_dissector.c
+++ b/net/core/flow_dissector.c
@@ -91,28 +91,31 @@ static inline void flow_dissector_count_fast(enum flow_dissector_shape shape)
/* IPv4 version/IHL byte of an option-less header: version 4, IHL 5. */
#define FLOW_DIS_IPV4_VIHL_NOOPT 0x45
-/* Fast-path helper forward declarations. */
+/* @num_hdrs: protocol headers consumed so far (Ethernet = 1, each VLAN
+ * tag, each outer IP header). Tunnel descents increment it and defer
+ * past MAX_FLOW_DISSECT_HDRS, so both paths stop at the same depth.
+ */
static bool flow_dissect_fast_ipv4(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen);
+ int nhoff, int hlen, int num_hdrs);
static bool flow_dissect_fast_ipv6(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen);
+ int nhoff, int hlen, int num_hdrs);
static bool flow_dissect_fast_ipip_inner(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
__be16 inner_eth_proto,
- int inner_nhoff, int hlen);
+ int inner_nhoff, int hlen, int num_hdrs);
static bool flow_dissect_fast_gre_inner(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen);
+ int nhoff, int hlen, int num_hdrs);
/* One of the two dissectors the fast-path eligibility check admits;
* defined here so flow_dissect_fast() below can reference it (its keys
@@ -1141,7 +1144,7 @@ static bool flow_dissect_fast_ipv4(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen)
+ int nhoff, int hlen, int num_hdrs)
{
struct flow_dissector_key_control *key_control;
struct flow_dissector_key_addrs *key_addrs;
@@ -1172,18 +1175,18 @@ static bool flow_dissect_fast_ipv4(const struct sk_buff *skb,
return flow_dissect_fast_ipip_inner(skb,
flow_dissector, target_container,
data, htons(ETH_P_IP),
- nhoff + (int)sizeof(*iph), hlen);
+ nhoff + (int)sizeof(*iph), hlen, num_hdrs);
if (iph->protocol == IPPROTO_IPV6)
return flow_dissect_fast_ipip_inner(skb,
flow_dissector, target_container,
data, htons(ETH_P_IPV6),
- nhoff + (int)sizeof(*iph), hlen);
+ nhoff + (int)sizeof(*iph), hlen, num_hdrs);
}
if (static_branch_unlikely(&flow_dissector_gre_key) &&
iph->protocol == IPPROTO_GRE)
return flow_dissect_fast_gre_inner(skb, flow_dissector,
target_container, data,
- nhoff + (int)sizeof(*iph), hlen);
+ nhoff + (int)sizeof(*iph), hlen, num_hdrs);
return false;
}
@@ -1241,7 +1244,7 @@ static bool flow_dissect_fast_ipv6(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen)
+ int nhoff, int hlen, int num_hdrs)
{
struct flow_dissector_key_control *key_control;
struct flow_dissector_key_addrs *key_addrs;
@@ -1317,10 +1320,10 @@ static bool flow_dissect_fast_ipv6(const struct sk_buff *skb,
data,
iph->nexthdr == IPPROTO_IPIP ?
htons(ETH_P_IP) : htons(ETH_P_IPV6),
- nhoff + (int)sizeof(*iph), hlen);
+ nhoff + (int)sizeof(*iph), hlen, num_hdrs);
return flow_dissect_fast_gre_inner(skb, flow_dissector,
target_container, data,
- nhoff + (int)sizeof(*iph), hlen);
+ nhoff + (int)sizeof(*iph), hlen, num_hdrs);
}
thoff = nhoff + (int)sizeof(*iph);
@@ -1439,12 +1442,12 @@ static bool flow_dissect_fast_vlan(const struct sk_buff *skb,
case htons(ETH_P_IP):
ok = flow_dissect_fast_ipv4(skb, flow_dissector,
target_container, data,
- nhoff, hlen);
+ nhoff, hlen, vlan_depth + 2);
break;
case htons(ETH_P_IPV6):
ok = flow_dissect_fast_ipv6(skb, flow_dissector,
target_container, data,
- nhoff, hlen);
+ nhoff, hlen, vlan_depth + 2);
break;
case htons(ETH_P_8021Q):
case htons(ETH_P_8021AD):
@@ -1524,11 +1527,11 @@ static bool flow_dissect_fast_pppoe(const struct sk_buff *skb,
if (inner_eth_proto == htons(ETH_P_IP))
ok = flow_dissect_fast_ipv4(skb, flow_dissector,
target_container, data,
- nhoff, hlen);
+ nhoff, hlen, 2);
else
ok = flow_dissect_fast_ipv6(skb, flow_dissector,
target_container, data,
- nhoff, hlen);
+ nhoff, hlen, 2);
if (ok)
flow_dissector_count_fast(FLOW_DISSECTOR_SHAPE_PPPOE);
@@ -1612,19 +1615,25 @@ static bool flow_dissect_fast_ipip_inner(const struct sk_buff *skb,
void *target_container,
const void *data,
__be16 inner_eth_proto,
- int inner_nhoff, int hlen)
+ int inner_nhoff, int hlen, int num_hdrs)
{
struct flow_dissector_key_control *key_control;
bool ok;
+ /* Mirror the slow path's MAX_FLOW_DISSECT_HDRS budget; past the cap
+ * defer, so both paths stop at the same depth.
+ */
+ if (++num_hdrs > MAX_FLOW_DISSECT_HDRS)
+ return false;
+
if (inner_eth_proto == htons(ETH_P_IP))
ok = flow_dissect_fast_ipv4(skb, flow_dissector,
target_container, data,
- inner_nhoff, hlen);
+ inner_nhoff, hlen, num_hdrs);
else if (inner_eth_proto == htons(ETH_P_IPV6))
ok = flow_dissect_fast_ipv6(skb, flow_dissector,
target_container, data,
- inner_nhoff, hlen);
+ inner_nhoff, hlen, num_hdrs);
else
return false;
@@ -1651,7 +1660,7 @@ static bool flow_dissect_fast_gre_inner(const struct sk_buff *skb,
struct flow_dissector *flow_dissector,
void *target_container,
const void *data,
- int nhoff, int hlen)
+ int nhoff, int hlen, int num_hdrs)
{
struct flow_dissector_key_control *key_control;
const struct gre_base_hdr *hdr;
@@ -1659,6 +1668,10 @@ static bool flow_dissect_fast_gre_inner(const struct sk_buff *skb,
int inner_nhoff;
bool ok;
+ /* Same MAX_FLOW_DISSECT_HDRS budget as flow_dissect_fast_ipip_inner(). */
+ if (++num_hdrs > MAX_FLOW_DISSECT_HDRS)
+ return false;
+
if (unlikely(hlen - nhoff < (int)sizeof(*hdr)))
return false;
hdr = (const struct gre_base_hdr *)((const u8 *)data + nhoff);
@@ -1686,11 +1699,11 @@ static bool flow_dissect_fast_gre_inner(const struct sk_buff *skb,
if (inner_proto == htons(ETH_P_IP))
ok = flow_dissect_fast_ipv4(skb, flow_dissector,
target_container, data,
- inner_nhoff, hlen);
+ inner_nhoff, hlen, num_hdrs);
else
ok = flow_dissect_fast_ipv6(skb, flow_dissector,
target_container, data,
- inner_nhoff, hlen);
+ inner_nhoff, hlen, num_hdrs);
if (!ok)
return false;
@@ -1764,7 +1777,7 @@ static bool flow_dissect_fast(const struct sk_buff *skb,
return false;
if (!flow_dissect_fast_ipv4(skb, flow_dissector,
target_container, data,
- nhoff, hlen))
+ nhoff, hlen, 1))
return false;
/* Plain top-level eth+IP only; descents count under their own shape. */
if (!flow_dissect_fast_is_encap(flow_dissector, target_container))
@@ -1775,7 +1788,7 @@ static bool flow_dissect_fast(const struct sk_buff *skb,
return false;
if (!flow_dissect_fast_ipv6(skb, flow_dissector,
target_container, data,
- nhoff, hlen))
+ nhoff, hlen, 1))
return false;
if (!flow_dissect_fast_is_encap(flow_dissector, target_container))
flow_dissector_count_fast(FLOW_DISSECTOR_SHAPE_ETH_IP);
--
2.54.0
next prev parent reply other threads:[~2026-07-16 0:44 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-16 0:43 [PATCH net-next v1 00/11] net: flow_dissector: opt-in byte-identical fast paths for common shapes Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 01/11] net: flow_dissector: gate BPF program lookup behind a static key Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 02/11] net: flow_dissector: opt-in fast-path for eth + IPv{4,6} + {TCP,UDP} Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 03/11] net: flow_dissector: add fast-path for VLAN and QinQ + IP + TCP/UDP Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 04/11] net: flow_dissector: add fast-path for PPPoE session + IPv{4,6} " Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 05/11] net: flow_dissector: add fast-path for single MPLS label + IP Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 06/11] net: flow_dissector: add fast-path for IP-in-IP family (IPIP / 4in6 / 6in4) Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 07/11] net: flow_dissector: add byte-identical fast-path for plain GRE inner Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 08/11] net: flow_dissector: per-shape counters + /proc/net/flow_dissector_stats Dave Seddon
2026-07-16 0:43 ` Dave Seddon [this message]
2026-07-16 0:43 ` [PATCH net-next v1 10/11] net: flow_dissector: add KUnit fast/slow path equivalence tests Dave Seddon
2026-07-16 0:43 ` [PATCH net-next v1 11/11] Documentation: networking: add flow_dissector overview and fast-path guide Dave Seddon
2026-07-16 9:50 ` [PATCH net-next v1 00/11] net: flow_dissector: opt-in byte-identical fast paths for common shapes Willem de Bruijn
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260716004357.3652679-10-dave.seddon.ca@gmail.com \
--to=dave.seddon.ca@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=sdf@fomichev.me \
--cc=tom@herbertland.com \
--cc=willemdebruijn.kernel@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.