clone_execute() only increments and decrements exec_level when clone_flow_key is true. When clone_flow_key is false the optimized path reuses the original flow key and calls do_execute_actions() directly without updating the recursion counter, making those layers invisible to the OVS_RECURSION_LIMIT check in ovs_execute_actions(). A single action tree allows up to OVS_COPY_ACTIONS_MAX_DEPTH (16) nested clone actions. When the innermost clone contains a RECIRC that selects another flow with its own 16-layer validation budget, multiple trees of unaccounted recursion can be stacked. The deferred action threshold (OVS_DEFERRED_ACTION_THRESHOLD = OVS_RECURSION_LIMIT - 2 = 3) allows three synchronous recirc levels before clone_key() forces deferral, yielding 3 x 16 = 48 layers of synchronous, unaccounted clone recursion. Per-layer stack usage is approximately 296 bytes (from disassembly: do_execute_actions() allocates 0x98 bytes, clone_execute() allocates 0x20 bytes, plus saved registers and return address). 48 layers consume ~14,208 bytes before recirc, flow lookup, Netlink, and base call frames, exceeding the 16 KiB x86_64 task stack and hitting the guard page. An unprivileged user can trigger this from a private user/net namespace (clone(CLONE_NEWUSER | CLONE_NEWNET)) by installing three flow rules each with 16 nested clone actions linked by RECIRC, then injecting a single packet via OVS_PACKET_CMD_EXECUTE. The result is a kernel panic: BUG: TASK stack guard page was hit ... CPU: 0 UID: 1000 PID: 85 ... 7.2.0-rc5+ RIP: 0010:clone_execute+0x5a/0x2c0 [openvswitch] [do_execute_actions / clone_execute alternating repeatedly] Kernel panic - not syncing: Fatal exception in interrupt Fix this by extracting the exec_level management into helpers ovs_exec_level_enter() and ovs_exec_level_exit(), and calling them unconditionally for all synchronous clone recursion edges. When the limit is exceeded the packet is dropped with a net_crit_ratelimited() log, consistent with the existing handling in ovs_execute_actions() which now also uses the same helpers. Tested on Linux 7.2.0-rc5+ (mainline HEAD 62cc90241548), x86_64, CONFIG_VMAP_STACK=y, CONFIG_OPENVSWITCH=m, isolated QEMU guest. Confirmed that the PoC (./poc_clone_overflow 2 16 3, run as UID 1000) no longer triggers a panic with this patch applied. Fixes: b233504033db ("openvswitch: kernel datapath clone action") Cc: stable@vger.kernel.org Reported-by: yang zhuorao Signed-off-by: yang zhuorao --- Changes in v2: - Extract exec_level management into ovs_exec_level_enter() / ovs_exec_level_exit() helpers, consolidating the recursion check with ovs_execute_actions() (Aaron) - Add net_crit_ratelimited() log on limit hit instead of silently dropping (Aaron) - Refactor ovs_execute_actions() to use the same helpers - CC netdev maintainers v1: https://lore.kernel.org/netdev/178521875706.803907.3226960398667307795@163.com Reproducer (C, single file, available upon request): gcc -O2 -Wall -o poc_clone_overflow poc_clone_overflow.c ./poc_clone_overflow 2 16 3 # as unprivileged user The PoC creates a private user/net namespace, installs three flows each with 16 nested clone actions linked by RECIRC, and injects a triggering packet. Prerequisites: CONFIG_OPENVSWITCH=m/y loaded, unprivileged user namespace creation allowed. Verified unfixed in Linus mainline (62cc90241548), net (97ac08560d23), and net-next (a50eba1e778a) as of 2026-07-27. net/openvswitch/actions.c | 44 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/net/openvswitch/actions.c b/net/openvswitch/actions.c index 513fca6a8e8a..dc17adbeb933 100644 --- a/net/openvswitch/actions.c +++ b/net/openvswitch/actions.c @@ -1466,6 +1466,26 @@ static int do_execute_actions(struct datapath *dp, struct sk_buff *skb, return 0; } +static int ovs_exec_level_enter(struct datapath *dp, struct sk_buff *skb) +{ + int level; + + level = __this_cpu_inc_return(ovs_pcpu_storage->exec_level); + if (unlikely(level > OVS_RECURSION_LIMIT)) { + net_crit_ratelimited("ovs: recursion limit reached on datapath %s, probable configuration error\n", + ovs_dp_name(dp)); + __this_cpu_dec(ovs_pcpu_storage->exec_level); + ovs_kfree_skb_reason(skb, OVS_DROP_RECURSION_LIMIT); + return -ENETDOWN; + } + return 0; +} + +static void ovs_exec_level_exit(void) +{ + __this_cpu_dec(ovs_pcpu_storage->exec_level); +} + /* Execute the actions on the clone of the packet. The effect of the * execution does not affect the original 'skb' nor the original 'key'. * @@ -1497,14 +1517,14 @@ static int clone_execute(struct datapath *dp, struct sk_buff *skb, if (clone) { int err = 0; if (actions) { /* Sample action */ - if (clone_flow_key) - __this_cpu_inc(ovs_pcpu_storage->exec_level); + err = ovs_exec_level_enter(dp, skb); + if (err) + return err; err = do_execute_actions(dp, skb, clone, actions, len); - if (clone_flow_key) - __this_cpu_dec(ovs_pcpu_storage->exec_level); + ovs_exec_level_exit(); } else { /* Recirc action */ clone->recirc_id = recirc_id; ovs_dp_process_packet(skb, clone); @@ -1571,14 +1591,11 @@ int ovs_execute_actions(struct datapath *dp, struct sk_buff *skb, { int err, level; - level = __this_cpu_inc_return(ovs_pcpu_storage->exec_level); - if (unlikely(level > OVS_RECURSION_LIMIT)) { - net_crit_ratelimited("ovs: recursion limit reached on datapath %s, probable configuration error\n", - ovs_dp_name(dp)); - ovs_kfree_skb_reason(skb, OVS_DROP_RECURSION_LIMIT); - err = -ENETDOWN; - goto out; - } + err = ovs_exec_level_enter(dp, skb); + if (err) + return err; + + level = __this_cpu_read(ovs_pcpu_storage->exec_level); OVS_CB(skb)->acts_origlen = acts->orig_len; err = do_execute_actions(dp, skb, key, @@ -1587,7 +1604,6 @@ int ovs_execute_actions(struct datapath *dp, struct sk_buff *skb, if (level == 1) process_deferred_actions(dp); -out: - __this_cpu_dec(ovs_pcpu_storage->exec_level); + ovs_exec_level_exit(); return err; } -- 2.43.0