Repository navigation
nat44-ei: 'nat44 ei del user' crashes VPP (SIGSEGV) with more than one worker #3749
Description
Summary
With more than one worker, nat44 ei del user <addr> (and the nat44_ei_del_user API) crashes VPP with SIGSEGV.
nat44_ei_user_del() looks the user up in each worker's per_thread_data[] and, when found, calls:
nat44_ei_user_del_sessions (pool_elt_at_index (tnm->users, value.value),
tnm->thread_index);tnm->thread_index is only assigned in nat44_ei_set_workers() (CLI set nat workers). Without that command it stays 0, so nat44_ei_user_del_sessions() uses the main thread's list_pool/sessions with the list head index of a user that lives on a worker:
- it walks foreign or freed sessions
nat44_ei_free_session_data()ends innat44_ei_free_outside_address_and_port()with an external address that is not innm->addresses- the
ASSERTthere is compiled out in release builds, so it dereferences past the vector and crashes
A second, smaller bug sits in the same command. nat44_ei_del_user_command_fn reports an error exactly when the deletion succeeds:
rv = nat44_ei_user_del (&addr, fib_index);
if (!rv)
error = clib_error_return (0, "nat44_ei_user_del returned %d", rv);Version and environment
vpp v26.06-release built by root on 4bf4caa1c4e5 at 2026-06-24T09:27:07
fd.io release packages (noble), x86_64.
- Seen in production with DPDK (mlx5) and 8 workers
- Reproduced below with af_packet and 2 workers
- The code is unchanged on master (
31cc55f72, 2026-10-10):nat44_ei.clines 1646 and 1658 usetnm->thread_index, line 263 is the only assignment, andnat44_ei_cli.cline 1404 has the inverted check
Reproduction
The script below needs only root, iproute2 and python3 (veth pairs, network namespaces, af_packet; no NICs). It starts a separate VPP instance with plugin default { disable } + af_packet + nat44_ei and 2 workers, creates a few UDP sessions from an inside namespace, and runs nat44 ei del user.
Result with the packaged nat44_ei_plugin.so (MAIN_CORE=12 WORKERS=13-14):
== workers
vpp_main vpp_wk_0 vpp_wk_1
== nat44 ei users / sessions
10.0.0.2: 1 dynamic translations, 0 static translations
== nat44 ei del user 10.0.0.2
... Aborted (core dumped) vpp -c ...
VPP CRASHED:
vpp[76757]: received signal SIGSEGV, PC 0x7bb06bb9514f, faulting address 0x7
With pnat also loaded (which fixes the #3747 arc order, so the sessions get real ports), there are 5 sessions and the crash is the same. In production the backtrace was:
#0 nat44_ei_free_outside_address_and_port + 0x66
#1 nat44_ei_free_session_data + 0x101
#2 nat44_ei_user_del + 0x15d
#3 nat44_ei_del_user_command_fn + 0x83
repro.sh
#!/bin/bash
# Reproduces: "nat44 ei del user" crashes VPP (SIGSEGV) when there is more than one worker.
# nat44_ei_user_del() finds the user in its worker's per_thread_data but calls
# nat44_ei_user_del_sessions (u, tnm->thread_index); tnm->thread_index is only set by
# nat44_ei_set_workers() ("set nat workers"), so it is 0 and the sessions are looked up in the
# main thread's pools.
# Needs root, iproute2 and python3. Uses veth + network namespaces and af_packet (no NICs).
# Usage: repro.sh [extra-plugin ...] (e.g. "repro.sh pnat" also fixes the #3747 arc order;
# the crash happens either way)
# Env: PLUGIN_DIR (default: the packaged plugins), MAIN_CORE / WORKERS (default 0 / 1-2).
set -u
D=$(mktemp -d /tmp/nat44ei-deluser.XXXX)
PLUGIN_DIR=${PLUGIN_DIR:-/usr/lib/x86_64-linux-gnu/vpp_plugins}
MAIN_CORE=${MAIN_CORE:-0}; WORKERS=${WORKERS:-1-2}
EXTRA=""; for p in "$@"; do EXTRA="$EXTRA plugin ${p}_plugin.so { enable }"; done
CLI="vppctl -s $D/cli.sock"
cleanup() {
[ -n "${VPP_PID:-}" ] && kill "$VPP_PID" 2>/dev/null && wait "$VPP_PID" 2>/dev/null
ip netns del nd-in 2>/dev/null; ip netns del nd-out 2>/dev/null
ip link del nd-vin 2>/dev/null; ip link del nd-vout 2>/dev/null
rm -rf "$D"
}
trap cleanup EXIT
# inside host 10.0.0.2 <-> VPP 10.0.0.1 | VPP 192.0.2.1 <-> outside host 192.0.2.2 ; pool 198.51.100.1
ip netns add nd-in; ip netns add nd-out
ip link add nd-vin type veth peer name in0 netns nd-in
ip link add nd-vout type veth peer name out0 netns nd-out
ip link set nd-vin up; ip link set nd-vout up
ip -n nd-in addr add 10.0.0.2/24 dev in0; ip -n nd-in link set in0 up; ip -n nd-in link set lo up
ip -n nd-in route add default via 10.0.0.1
ip -n nd-out addr add 192.0.2.2/24 dev out0; ip -n nd-out link set out0 up; ip -n nd-out link set lo up
ip -n nd-out route add 198.51.100.0/24 via 192.0.2.1
cat > "$D/startup.conf" <<EOF
unix { cli-listen $D/cli.sock nodaemon log $D/vpp.log }
api-segment { prefix nd$$ }
socksvr { socket-name $D/api.sock }
statseg { socket-name $D/stats.sock size 32m page-size 4k }
cpu { main-core $MAIN_CORE corelist-workers $WORKERS }
memory { main-heap-size 512M main-heap-page-size 4k }
buffers { buffers-per-numa 16384 page-size 4k }
plugins {
path $PLUGIN_DIR
plugin default { disable }
plugin af_packet_plugin.so { enable }
plugin nat44_ei_plugin.so { enable }
$EXTRA
}
EOF
vpp -c "$D/startup.conf" > "$D/stdout" 2>&1 & VPP_PID=$!
for i in $(seq 1 50); do $CLI show version >/dev/null 2>&1 && break; sleep 0.2; done
$CLI create host-interface name nd-vin >/dev/null
$CLI create host-interface name nd-vout >/dev/null
$CLI set interface state host-nd-vin up
$CLI set interface state host-nd-vout up
$CLI set interface ip address host-nd-vin 10.0.0.1/24
$CLI set interface ip address host-nd-vout 192.0.2.1/24
$CLI nat44 ei plugin enable sessions 1024
$CLI nat44 ei add address 198.51.100.1
$CLI set interface nat44 ei in host-nd-vin out host-nd-vout
echo "== show version"; $CLI show version
echo "== workers"; $CLI show threads | awk 'NR>1 {print " " $2}' | tr '\n' ' '; echo
echo "== 5 UDP flows from the inside host"
ip netns exec nd-in python3 -c '
import socket
for p in range(40000, 40005):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.bind(("10.0.0.2", p))
s.sendto(b"x", ("192.0.2.2", 7000))'
sleep 1
echo "== nat44 ei users / sessions"
$CLI show nat44 ei sessions | grep -E "^ +10\.0\.0\.2"
echo "== nat44 ei del user 10.0.0.2"
$CLI nat44 ei del user 10.0.0.2
sleep 1
if kill -0 "$VPP_PID" 2>/dev/null; then
echo "VPP still running"
else
echo "VPP CRASHED:"; grep -E "SIGSEGV|^#[0-9]" "$D/stdout" | head -8
fiSuggested fix
The worker index is the position of tnm in per_thread_data:
@@ nat44_ei_user_del (ip4_address_t *addr, u32 fib_index)
if (!clib_bihash_search_8_8 (&tnm->user_hash, &kv, &value))
{
nat44_ei_user_del_sessions (
pool_elt_at_index (tnm->users, value.value),
- tnm->thread_index);
+ tnm - nm->per_thread_data);
rv = 0;
break;
}
@@
if (!clib_bihash_search_8_8 (&tnm->user_hash, &kv, &value))
{
nat44_ei_user_del_sessions (
- pool_elt_at_index (tnm->users, value.value), tnm->thread_index);
+ pool_elt_at_index (tnm->users, value.value),
+ tnm - nm->per_thread_data);
rv = 0;
}@@ nat44_ei_del_user_command_fn
rv = nat44_ei_user_del (&addr, fib_index);
- if (!rv)
+ if (rv)
{
- error = clib_error_return (0, "nat44_ei_user_del returned %d", rv);
+ error = clib_error_return (0, "no such user");
}tnm->thread_index has no other users, so it could also simply be removed. We carry this fix in our tree and verified that the crash is gone, with and without pnat, with 2 workers (lab) and 8 workers (production).
Activity