selftests/tc-testing: Add DualPI2 GSO backlog accounting test

Add a regression test for DualPI2 GSO backlog accounting when it is
used as a child qdisc of QFQ.

The test sends one UDP GSO datagram through a QFQ class with DualPI2 as
the leaf qdisc. DualPI2 splits the skb into two segments. After the
traffic drains, both QFQ and DualPI2 must report zero backlog and zero
qlen.

On kernels with the broken accounting, QFQ can keep a stale non-zero
qlen after all real packets have been dequeued.

Signed-off-by: Xingquan Liu <b1n@b1n.io>
Acked-by: Jamal Hadi Salim <jhs@mojatatu.com>
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Link: https://patch.msgid.link/20260619151447.223640-2-b1n@b1n.io
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Xingquan Liu 2026-06-19 11:13:48 -04:00 committed by Jakub Kicinski
parent 05ed733b65
commit 54704b32b2
2 changed files with 87 additions and 0 deletions

View File

@ -250,5 +250,49 @@
"teardown": [
"$TC qdisc del dev $DUMMY handle 1: root"
]
},
{
"id": "891f",
"name": "Verify DualPI2 GSO backlog accounting with QFQ parent",
"category": [
"qdisc",
"dualpi2",
"qfq",
"gso"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
"$IP link set dev $DUMMY up || true",
"$IP addr add 10.10.10.10/24 dev $DUMMY || true",
"$TC qdisc add dev $DUMMY root handle 1: qfq",
"$TC class add dev $DUMMY parent 1: classid 1:1 qfq weight 1 maxpkt 4096",
"$TC qdisc add dev $DUMMY parent 1:1 handle 2: dualpi2",
"$TC filter add dev $DUMMY parent 1: matchall classid 1:1"
],
"cmdUnderTest": "./tdc_gso.py 10.10.10.10 10.10.10.1 9000 1200 2400",
"expExitCode": "0",
"verifyCmd": "$TC -j -s qdisc ls dev $DUMMY",
"matchJSON": [
{
"kind": "qfq",
"handle": "1:",
"packets": 2,
"backlog": 0,
"qlen": 0
},
{
"kind": "dualpi2",
"handle": "2:",
"packets": 2,
"backlog": 0,
"qlen": 0
}
],
"teardown": [
"$TC qdisc del dev $DUMMY root",
"$IP addr del 10.10.10.10/24 dev $DUMMY || true"
]
}
]

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
tdc_gso.py - send a UDP GSO datagram
Copyright (C) 2026 Xingquan Liu <b1n@b1n.io>
"""
import argparse
import socket
import struct
import sys
UDP_MAX_SEGMENTS = 1 << 7
parser = argparse.ArgumentParser(description="UDP GSO datagram sender")
parser.add_argument("src", help="source IPv4 address")
parser.add_argument("dst", help="destination IPv4 address")
parser.add_argument("port", type=int, help="destination UDP port")
parser.add_argument("gso_size", type=int, help="UDP GSO segment payload size")
parser.add_argument("payload_len", type=int, help="total UDP payload length")
args = parser.parse_args()
if args.gso_size <= 0 or args.gso_size > 0xFFFF:
parser.error("gso_size must fit in an unsigned 16-bit integer")
if args.payload_len <= args.gso_size:
parser.error("payload_len must be larger than gso_size")
if args.payload_len > args.gso_size * UDP_MAX_SEGMENTS:
parser.error("payload_len exceeds UDP_MAX_SEGMENTS")
SOL_UDP = getattr(socket, "SOL_UDP", socket.IPPROTO_UDP)
UDP_SEGMENT = getattr(socket, "UDP_SEGMENT", 103)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((args.src, 0))
payload = b"b" * args.payload_len
cmsg = [(SOL_UDP, UDP_SEGMENT, struct.pack("=H", args.gso_size))]
sent = sock.sendmsg([payload], cmsg, 0, (args.dst, args.port))
sys.exit(sent != len(payload))