selftests: nft_queue.sh: add a parallel stress test

Introduce a new stress test to check for race conditions in the
nfnetlink_queue subsystem, where an entry is freed while another CPU is
concurrently walking the global rhashtable.

To trigger this, `nf_queue.c` is extended with two new flags:
  * -O (out-of-order): Buffers packet IDs and flushes them in reverse.
  * -b (bogus verdicts): Floods the kernel with non-existent packet IDs.

The bogus verdict loop forces the kernel's lookup function to perform
full rhashtable bucket traversals (-ENOENT). Combined with reverse-order
flushing and heavy parallel UDP/ping flooding across 8 queues, this puts
the nfnetlink_queue code under pressure.

Joint work with Florian Westphal.

Signed-off-by: Fernando Fernandez Mancera <fmancera@suse.de>
Signed-off-by: Florian Westphal <fw@strlen.de>
This commit is contained in:
Fernando Fernandez Mancera 2026-04-06 23:18:31 +02:00 committed by Florian Westphal
parent 936206e3f6
commit dde1a6084c
2 changed files with 115 additions and 18 deletions

View File

@ -19,6 +19,8 @@ struct options {
bool count_packets;
bool gso_enabled;
bool failopen;
bool out_of_order;
bool bogus_verdict;
int verbose;
unsigned int queue_num;
unsigned int timeout;
@ -31,7 +33,7 @@ static struct options opts;
static void help(const char *p)
{
printf("Usage: %s [-c|-v [-vv] ] [-o] [-t timeout] [-q queue_num] [-Qdst_queue ] [ -d ms_delay ] [-G]\n", p);
printf("Usage: %s [-c|-v [-vv] ] [-o] [-O] [-b] [-t timeout] [-q queue_num] [-Qdst_queue ] [ -d ms_delay ] [-G]\n", p);
}
static int parse_attr_cb(const struct nlattr *attr, void *data)
@ -275,7 +277,9 @@ static int mainloop(void)
unsigned int buflen = 64 * 1024 + MNL_SOCKET_BUFFER_SIZE;
struct mnl_socket *nl;
struct nlmsghdr *nlh;
uint32_t ooo_ids[16];
unsigned int portid;
int ooo_count = 0;
char *buf;
int ret;
@ -308,6 +312,9 @@ static int mainloop(void)
ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, NULL);
if (ret < 0) {
/* bogus verdict mode will generate ENOENT error messages */
if (opts.bogus_verdict && errno == ENOENT)
continue;
perror("mnl_cb_run");
exit(EXIT_FAILURE);
}
@ -316,10 +323,35 @@ static int mainloop(void)
if (opts.delay_ms)
sleep_ms(opts.delay_ms);
nlh = nfq_build_verdict(buf, id, opts.queue_num, opts.verdict);
if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
if (opts.bogus_verdict) {
for (int i = 0; i < 50; i++) {
nlh = nfq_build_verdict(buf, id + 0x7FFFFFFF + i,
opts.queue_num, opts.verdict);
mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
}
}
if (opts.out_of_order) {
ooo_ids[ooo_count] = id;
if (ooo_count >= 15) {
for (ooo_count; ooo_count >= 0; ooo_count--) {
nlh = nfq_build_verdict(buf, ooo_ids[ooo_count],
opts.queue_num, opts.verdict);
if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
}
}
ooo_count = 0;
} else {
ooo_count++;
}
} else {
nlh = nfq_build_verdict(buf, id, opts.queue_num, opts.verdict);
if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
}
}
}
@ -332,7 +364,7 @@ static void parse_opts(int argc, char **argv)
{
int c;
while ((c = getopt(argc, argv, "chvot:q:Q:d:G")) != -1) {
while ((c = getopt(argc, argv, "chvoObt:q:Q:d:G")) != -1) {
switch (c) {
case 'c':
opts.count_packets = true;
@ -375,6 +407,12 @@ static void parse_opts(int argc, char **argv)
case 'v':
opts.verbose++;
break;
case 'O':
opts.out_of_order = true;
break;
case 'b':
opts.bogus_verdict = true;
break;
}
}

View File

@ -11,6 +11,7 @@ ret=0
timeout=5
SCTP_TEST_TIMEOUT=60
STRESS_TEST_TIMEOUT=30
cleanup()
{
@ -719,6 +720,74 @@ EOF
fi
}
check_tainted()
{
local msg="$1"
if [ "$tainted_then" -ne 0 ];then
return
fi
read tainted_now < /proc/sys/kernel/tainted
if [ "$tainted_now" -eq 0 ];then
echo "PASS: $msg"
else
echo "TAINT: $msg"
dmesg
ret=1
fi
}
test_queue_stress()
{
read tainted_then < /proc/sys/kernel/tainted
local i
ip netns exec "$nsrouter" nft -f /dev/stdin <<EOF
flush ruleset
table inet t {
chain forward {
type filter hook forward priority 0; policy accept;
queue flags bypass to numgen random mod 8
}
}
EOF
timeout "$STRESS_TEST_TIMEOUT" ip netns exec "$ns2" \
socat -u UDP-LISTEN:12345,fork,pf=ipv4 STDOUT > /dev/null &
timeout "$STRESS_TEST_TIMEOUT" ip netns exec "$ns3" \
socat -u UDP-LISTEN:12345,fork,pf=ipv4 STDOUT > /dev/null &
for i in $(seq 0 7); do
ip netns exec "$nsrouter" timeout "$STRESS_TEST_TIMEOUT" \
./nf_queue -q $i -t 2 -O -b > /dev/null &
done
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
ping -q -f 10.0.2.99 > /dev/null 2>&1 &
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
ping -q -f 10.0.3.99 > /dev/null 2>&1 &
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
ping -q -f "dead:2::99" > /dev/null 2>&1 &
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
ping -q -f "dead:3::99" > /dev/null 2>&1 &
busywait "$BUSYWAIT_TIMEOUT" udp_listener_ready "$ns2" 12345
busywait "$BUSYWAIT_TIMEOUT" udp_listener_ready "$ns3" 12345
for i in $(seq 1 4);do
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
socat -u STDIN UDP-DATAGRAM:10.0.2.99:12345 < /dev/zero > /dev/null &
ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \
socat -u STDIN UDP-DATAGRAM:10.0.3.99:12345 < /dev/zero > /dev/null &
done
wait
check_tainted "concurrent queueing"
}
test_queue_removal()
{
read tainted_then < /proc/sys/kernel/tainted
@ -742,18 +811,7 @@ EOF
ip netns exec "$ns1" nft flush ruleset
if [ "$tainted_then" -ne 0 ];then
return
fi
read tainted_now < /proc/sys/kernel/tainted
if [ "$tainted_now" -eq 0 ];then
echo "PASS: queue program exiting while packets queued"
else
echo "TAINT: queue program exiting while packets queued"
dmesg
ret=1
fi
check_tainted "queue program exiting while packets queued"
}
ip netns exec "$nsrouter" sysctl net.ipv6.conf.all.forwarding=1 > /dev/null
@ -799,6 +857,7 @@ test_sctp_forward
test_sctp_output
test_udp_nat_race
test_udp_gro_ct
test_queue_stress
# should be last, adds vrf device in ns1 and changes routes
test_icmp_vrf