Re: [net] net: openvswitch: fix stack exhaustion from unaccounted clone_execute() recursion
From: Aaron Conole <aconole@redhat.com>
Date: 2026-07-29 17:30:51
Eelco Chaudron [off-list ref] writes:
On 29 Jul 2026, at 11:56, Ilya Maximets wrote:quoted
On 7/28/26 11:01 PM, Aaron Conole wrote:quoted
Ilya Maximets [off-list ref] writes:quoted
On 7/28/26 4:51 PM, Aaron Conole wrote:quoted
Hi Yang, yang zhuorao [off-list ref] writes:quoted
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 unconditionally incrementing exec_level for all synchronous clone recursion edges and checking OVS_RECURSION_LIMIT before calling do_execute_actions(). When the limit is exceeded the packet is dropped and -ENETDOWN is returned, consistent with the existing handling in ovs_execute_actions(). 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 <redacted> Signed-off-by: yang zhuorao <redacted> --- 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 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) --General - please remember to CC the netdev maintainers as well. [...]quoted
@@ -1497,14 +1497,19 @@ 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); + __this_cpu_inc(ovs_pcpu_storage->exec_level); + + if (unlikely(__this_cpu_read(ovs_pcpu_storage->exec_level) > + OVS_RECURSION_LIMIT)) { + __this_cpu_dec(ovs_pcpu_storage->exec_level); + ovs_kfree_skb_reason(skb, OVS_DROP_RECURSION_LIMIT); + return -ENETDOWN; + }There is an existing check for recursion in ovs_execute_actions() and in there we have a log before dropping: net_crit_ratelimited("ovs: recursion limit reached on datapath %s, probable configuration error\n", ovs_dp_name(dp)); We should have a similar log here so we're not silently dropping. But, since we now have two places where this check occurs (and it's complicated because of how deferred actions is processed), maybe it's better to have a function that does the management: 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)); 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); } Then just put the enter/exit calls in the clone_execute() and ovs_execute_actions() spaces. That way if we need to adjust this limit in the future, it's consolidated to just one place.There is a different issue here though. The exec_level is used as a counter for the flow keys during the clone. And if we increment it unconditionally while not allocating new keys, we'll ran out of key slots much faster without actually using them. This will lead to much more actions to be deferred and packets dropped in the end. There is already a mismatch here as ovs_execute_actions() increments the level without allocating the key, but doing so also for non-recirc cases would significantly amplify the problem.That's an additional concern, but the code path at least currently handles the event that we advance beyond the clone flow key per-cpu array. This change makes a low-likelihood scenario a bit worse (the case where we have lots of clones + sub-actions). I don't think it needs to be addressed with this work since this is a security fix, but we should address it via a follow up change (and I can work on that if needed). AFAICT the affected actions are: - sample() - clone() - dec_ttl() - check_pkt_len() I think in most deployments I've seen they only nest once or twice, and we should have 3 'nests' worth of clone flow keys to handle it, meaning that the increased pressure should still fit within the slots we have (we just wouldn't support another nest, which is different from today): ie: nest 1, we are 'taking' slot 2 (key[1]), and nest 2 we are 'taking' slot 3 (key[2]), which leaves no more available. For example, check_pkt_len(..., check_pkt_len(...)) would still work as long as there's no sample() wrapped around it, or a dec_ttl within it, or some other more complex scenario. And it seems to be very heavy handed to ask for a fix that includes both the stack overflow and the clone allocation change together.While direct nesting is indeed not common, it is very common to have recirculation actions inside the clones or check_pkt_len, e.g.: check_pkt_len(1514, le(recirc(X)), gt(recirc(Y))) Before this patch we'd only count one level, which is recirculation itself. After this change it will be two levels. A lot of production setups hover at 3-4 recirculations on certain packet paths, e.g. pod-to-external traffic in ovn-kubernetes. And these setups do have at least a few clones or check_pkt_len actions on these paths. And so, if we start counting plain clones towards the limit, we could easily break real world deployments, given the limit is just 5.
ACK - I wasn't considering something like a bare recirc chain mixed into a separate instance of clone key allocations. Okay then, we will need both changes then. A proper clone key allocation that is decoupled from the recursion depth needs to be included with this change.
quoted
So, we need to reconsider the limits here. But we also can't blindly increase the limit as it will increase per-CPU memory consumption since that number dictates the size of the storage for keys, which are large.quoted
quoted
We likely need to decouple the recursion level couter from the slot index in the keys array. Though they still should stay related as we should start deferring when we approach recursion limit even if we still have some free slots for key clones.Agreed, we need to have two discreet operations - one to track the stack recursion and one to do the clone flow key allocation. Unless you feel strongly that they should come as part of the same series, but it seems a lot.I think, it should be done within this patch, because otherwise we'll be most likely breaking existing deployments. We need a few things: 1. Decouple the key array index from the execution level. 2. Potentially increase the recursion limit, since it is going to count more things. x2 might be a safe choice, but it is also quite a lot and we're risking to ran out of stack in other scenarios. Something like +3 may be a change to explore. We will need extra testing here to make sure we're not breaking existing setups. I could try and check ovn-kubernetes deployments, as an example of the most demanding configuration, but it will take a bit of time.
I'm not sure increasing the limits is the right path. Keys are already quite large. I think multiplying it would be too much. You're right that incrementing the limit by 3 might be acceptable but I think keeping around the overloaded counter is still a problem.
quoted
3. Sahiko also points out that we should not be dropping packets in case we reached the limit inside the clone_execute(), we should attempt to defer the actions first. In which case we should not log that the limit is reached.
Yes. Because the limits are trying to track two things together (recursion depth and the clone-key-pool), it makes it a bit more difficult to make a single helper that does the counting right - we'd need to pass more state from the callers.
quoted
WDYT? Eelco, do you have some thoughts on this?I agree we should not blindly remove the 'if (clone_flow_key)' guard from the exec_level increment and assume everything will work correctly. We have seen issues with this code in the past. One approach would be to introduce a second counter, clone_nest_level, to take over the role that exec_level currently plays for key/slot allocation and deferral triggering. exec_level could then be incremented unconditionally for all synchronous do_execute_actions() calls, serving as a stack-depth safeguard, while clone_nest_level preserves the existing deferral semantics.
I think having a second allocation routine that uses a separate counter is the path forward, based on the discussion.
For this patch, clone_nest_level could mirror the current exec_level behaviour, i.e., increment when clone_flow_key is true, use the value as the index into the flow_keys slot array, and defer when the slot array is exhausted. That would close the stack overflow window without changing deferral behaviour for existing deployments.
We should make sure that the names make sense. Something more like clone_key_next is probably fine - I don't think we need a 'level' as that may be confusing while reviewing. We have the clone_key() to allocate, and maybe we just need something like clone_key_release() to release the last allocated one by decrementing the counter back down.
As a follow-up patch we could refine clone_nest_level so that it only advances when a key slot is actually allocated rather than on every clone_flow_key=true entry.
If we're going to be defining the new counter anyway, we shouldn't try to add it as a stop-gap measure. I think in this case, we should implement it completely the first time (whatever we can agree is right). I think if we introduce the counter for the clone key allocations, we should only advance it when we actually allocate a clone key. This means we don't need to be 1-based for the allocation sematic anymore either.
Have not tried to code this, but seems doable.
+1
//Eelco