A TCP GSO skb created through TUN can retain an unset transport header. Send one such packet through TBF, then send another through police and TBF. The police limit makes the vulnerable kernel stop at one TBF drop while the fixed kernel reaches two. This checks behavior without relying on KASAN or global logs.
Use the existing tc-testing namespace and JSON verification. The helper creates the TUN packet, changes the ingress filter, and waits for TBF counters. Assisted-by: LLM Signed-off-by: Weiming Shi <[email protected]> --- tools/testing/selftests/tc-testing/config | 3 + .../tc-testing/tc-tests/qdiscs/tbf.json | 28 ++++++ .../testing/selftests/tc-testing/tdc_vnet.py | 87 +++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tools/testing/selftests/tc-testing/tdc_vnet.py diff --git a/tools/testing/selftests/tc-testing/config b/tools/testing/selftests/tc-testing/config index 0e5618be03359..7d1a140464948 100644 --- a/tools/testing/selftests/tc-testing/config +++ b/tools/testing/selftests/tc-testing/config @@ -5,6 +5,9 @@ CONFIG_DUMMY=y CONFIG_VETH=y CONFIG_IFB=y +CONFIG_TUN=y +CONFIG_DEBUG_KERNEL=y +CONFIG_DEBUG_NET=y # # Core Netfilter Configuration diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/tbf.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/tbf.json index 547a449100411..ef850a5d28ffe 100644 --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/tbf.json +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/tbf.json @@ -189,5 +189,33 @@ "teardown": [ "$TC qdisc del dev $DUMMY handle 1: root" ] + }, + { + "id": "7f31", + "name": "Drop GSO packet with unset transport header", + "category": [ + "qdisc", + "tbf" + ], + "plugins": { + "requires": "nsPlugin" + }, + "setup": [ + "$IP link set dev $IFB mtu 256", + "$TC qdisc add dev $IFB handle 1: root tbf limit 4096 burst 300 rate 1mbit" + ], + "cmdUnderTest": "python3 ./tdc_vnet.py $IP $TC $IFB", + "expExitCode": "0", + "verifyCmd": "$TC -s -j qdisc show dev $IFB root", + "matchJSON": [ + { + "kind": "tbf", + "handle": "1:", + "drops": 2 + } + ], + "teardown": [ + "$TC qdisc del dev $IFB handle 1: root" + ] } ] diff --git a/tools/testing/selftests/tc-testing/tdc_vnet.py b/tools/testing/selftests/tc-testing/tdc_vnet.py new file mode 100644 index 0000000000000..bdd663c5795ac --- /dev/null +++ b/tools/testing/selftests/tc-testing/tdc_vnet.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""Exercise TBF and police with a TCP GSO skb lacking a transport header.""" + +import fcntl +import json +import os +import struct +import subprocess +import sys +import time + + +# These architectures use a different _IOW direction encoding. +TUNSETIFF = (0x800454ca if os.uname().machine.startswith( + ("alpha", "hppa", "mips", "parisc", "ppc", "sparc")) else 0x400454ca) +IFF_TUN = 0x0001 +IFF_NO_PI = 0x1000 +IFF_VNET_HDR = 0x4000 +TUN = "tuntdc0" +DEADLINE = time.monotonic() + 18 + + +def run(*argv): + remaining = DEADLINE - time.monotonic() + if remaining <= 0: + raise RuntimeError("selftest deadline expired") + return subprocess.check_output(argv, stderr=subprocess.STDOUT, + text=True, timeout=min(3, remaining)) + + +def tbf_drops(tc, ifb): + qdiscs = json.loads(run(tc, "-s", "-j", "qdisc", "show", "dev", ifb, + "root")) + return next(qdisc["drops"] for qdisc in qdiscs + if qdisc["kind"] == "tbf" and qdisc["handle"] == "1:") + + +def wait_for_drops(tc, ifb, expected): + deadline = min(DEADLINE, time.monotonic() + 5) + while time.monotonic() < deadline: + if tbf_drops(tc, ifb) >= expected: + return + time.sleep(0.05) + raise RuntimeError(f"TBF did not reach {expected} drops") + + +def main(ip, tc, ifb): + tun = os.open("/dev/net/tun", os.O_RDWR | os.O_CLOEXEC | os.O_NONBLOCK) + try: + ifreq = struct.pack("16sH", TUN.encode(), + IFF_TUN | IFF_NO_PI | IFF_VNET_HDR) + fcntl.ioctl(tun, TUNSETIFF, ifreq) + run(ip, "link", "set", "dev", TUN, "up") + run(tc, "qdisc", "add", "dev", TUN, "clsact") + run(tc, "filter", "add", "dev", TUN, "ingress", "pref", "1", + "matchall", "action", "mirred", "egress", "redirect", + "dev", ifb) + + # GSO without NEEDS_CSUM leaves transport_header unset. IPv4 IHL=0 + # prevents the later transport-header probe from filling it in. + packet = struct.pack("=BBHHHH", 0, 1, 0, 8, 0, 0) + b"\x40" + bytes(999) + if os.write(tun, packet) != len(packet): + raise RuntimeError("short TUN write") + wait_for_drops(tc, ifb, 1) + + run(tc, "filter", "delete", "dev", TUN, "ingress", "pref", "1") + run(tc, "filter", "add", "dev", TUN, "ingress", "pref", "1", + "matchall", "action", "police", "mtu", "65700", + "conform-exceed", "pipe/drop", "action", "mirred", "egress", + "redirect", "dev", ifb) + if os.write(tun, packet) != len(packet): + raise RuntimeError("short TUN write") + wait_for_drops(tc, ifb, 2) + finally: + os.close(tun) + + +if __name__ == "__main__": + if len(sys.argv) != 4: + sys.exit(f"usage: {sys.argv[0]} IP TC IFB") + try: + main(*sys.argv[1:]) + except (OSError, ValueError, RuntimeError, StopIteration, + subprocess.SubprocessError) as error: + sys.exit(f"tdc_vnet: {error}") -- 2.55.0

