Merge branch 'net-cap-tx_queue_len-at-s16_max-to-prevent-oversized-ring-allocations'

Jamal Hadi Salim says:

====================
net: cap tx_queue_len at S16_MAX to prevent oversized ring allocations

An unprivileged user (via unshare -Urn) can set a huge tx_queue_len
and exhaust global memory through ring allocations sized from it
(pfifo_fast skb_arrays, tun/tap ptr_rings).
The reproducer from vega@nebusec.ai set the following params for
illustration: txqlen of 500000 -> ~32 GiB/ring attempts, 1.6 GB tun,
~960 MB tap. Gets worse when you consider qdiscs like mq.

What we fix: every path an unprivileged user can use to install
an oversized tx_queue_len is rejected with -ERANGE before any ring is
allocated; per-ring memory is bounded at 256 KiB.

This is for you sashikos: What we deliberately _do not fix_
bound the NUMBER of rings. With the cap in place the worst case moves
from "one knob" to the aggregate of ring x queues x devices, example:

  ip link add v0 numtxqueues 4096 txqueuelen 32767 type veth
  tc qdisc add dev v0 root mq
    -> 4096 * 3 * 32767 * 8 = ~3.0 GiB (one command)
  50 tun devices x 256 queues x 32767 x 8 = ~3.1 GiB

Unfortunately tx_queue_len is a bit ambigious in meaning:
In some cases it means a ring size (which is pre-allocated, ex:
tun, tap, and pfifo_fast); a cap of 4096 seems reasonable here.
but in other cases it is used to indicate a queue limit ex:
the qdisc consumers that allocate nothing (pfifo/bfifo/gred/plug/sfb,
htb direct_qlen, qfq, teql). 32767 is a legitimate high-BDP queue
length, so we are going to keep that value.

Getting back to you sashikos, after this is merged and shows up
in net-next we will send followup patches as follows:
this series is not misread as "closes the OOM class"):

a) Per-site ring limits at six identified locations
    - pfifo_fast init/resize,
    - tun attach/resize,
    - tap minor/resize)

   if you can spot more in your review we will take care of those as well.

b) memcg accounting (GFP_KERNEL_ACCOUNT) for those ring
   allocations: contains a memcg-limited container's ring memory.
   Not GFP_KERNEL_ACCOUNT has no effect on the unshare attacker
   but will protect against containers  (memory.max in its cgroup)
====================

Link: https://patch.msgid.link/QDISC-2899.v2.20260901233641@mojatatu.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2026-09-04 16:30:53 -07:00
commit 6262acad9d
4 changed files with 213 additions and 2 deletions

View File

@ -898,6 +898,8 @@ attribute-sets:
-
name: txqlen
type: u32
checks:
max: 32767
-
name: map
type: binary

View File

@ -9982,7 +9982,7 @@ int netif_change_tx_queue_len(struct net_device *dev, unsigned long new_len)
unsigned int orig_len = dev->tx_queue_len;
int res;
if (new_len != (unsigned int)new_len)
if (new_len > S16_MAX)
return -ERANGE;
if (new_len != orig_len) {

View File

@ -2287,6 +2287,11 @@ static int rtnl_fill_ifinfo(struct sk_buff *skb,
return -EMSGSIZE;
}
static const struct netlink_range_validation txqlen_range = {
.min = 0,
.max = S16_MAX,
};
static const struct nla_policy ifla_policy[IFLA_MAX+1] = {
[IFLA_UNSPEC] = { .strict_start_type = IFLA_DPLL_PIN },
[IFLA_IFNAME] = { .type = NLA_STRING, .len = IFNAMSIZ-1 },
@ -2297,7 +2302,7 @@ static const struct nla_policy ifla_policy[IFLA_MAX+1] = {
[IFLA_LINK] = { .type = NLA_U32 },
[IFLA_MASTER] = { .type = NLA_U32 },
[IFLA_CARRIER] = { .type = NLA_U8 },
[IFLA_TXQLEN] = { .type = NLA_U32 },
[IFLA_TXQLEN] = NLA_POLICY_FULL_RANGE(NLA_U32, &txqlen_range),
[IFLA_WEIGHT] = { .type = NLA_U32 },
[IFLA_OPERSTATE] = { .type = NLA_U8 },
[IFLA_LINKMODE] = { .type = NLA_U8 },

View File

@ -105,5 +105,209 @@
"teardown": [
"$TC qdisc del dev $DUMMY handle 1: root"
]
},
{
"id": "dbe3",
"name": "Set tx_queue_len to S16_MAX boundary (32767 accepted)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [],
"cmdUnderTest": "$IP link set dev $DUMMY txqueuelen 32767",
"expExitCode": "0",
"verifyCmd": "$IP link show dev $DUMMY",
"matchPattern": "qlen 32767$",
"matchCount": "1",
"teardown": []
},
{
"id": "b50e",
"name": "Reject tx_queue_len above S16_MAX at set time (32768)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [],
"cmdUnderTest": "$IP link set dev $DUMMY txqueuelen 32768",
"expExitCode": "2",
"verifyCmd": "$IP link show dev $DUMMY",
"matchPattern": "qlen 1000$",
"matchCount": "1",
"teardown": []
},
{
"id": "40f8",
"name": "Reject tx_queue_len above S16_MAX via sysfs (32768)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [],
"cmdUnderTest": "sh -c 'echo 32768 > /sys/class/net/$DUMMY/tx_queue_len'",
"expExitCode": "1",
"verifyCmd": "$IP link show dev $DUMMY",
"matchPattern": "qlen 1000$",
"matchCount": "1",
"teardown": []
},
{
"id": "4b6e",
"name": "Set tx_queue_len to S16_MAX via sysfs (32767 accepted)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [],
"cmdUnderTest": "sh -c 'echo 32767 > /sys/class/net/$DUMMY/tx_queue_len'",
"expExitCode": "0",
"verifyCmd": "$IP link show dev $DUMMY",
"matchPattern": "qlen 32767$",
"matchCount": "1",
"teardown": []
},
{
"id": "b90d",
"name": "Create device with tx_queue_len at S16_MAX boundary (32767 accepted)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
[
"$IP link del dev $DUMMY",
0,
1
]
],
"cmdUnderTest": "$IP link add dev $DUMMY txqueuelen 32767 type dummy",
"expExitCode": "0",
"verifyCmd": "$IP link show dev $DUMMY",
"matchPattern": "qlen 32767$",
"matchCount": "1",
"teardown": [
[
"$IP link del dev $DUMMY",
0,
1
]
]
},
{
"id": "57ab",
"name": "Reject creating device with tx_queue_len above S16_MAX (32768)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
[
"$IP link del dev $DUMMY",
0,
1
]
],
"cmdUnderTest": "$IP link add dev $DUMMY txqueuelen 32768 type dummy",
"expExitCode": "2",
"verifyCmd": "$IP -o link show",
"matchPattern": "^[0-9]+: $DUMMY",
"matchCount": "0",
"teardown": []
},
{
"id": "e777",
"name": "Reject creating device with oversized tx_queue_len (500000)",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
[
"$IP link del dev $DUMMY",
0,
1
]
],
"cmdUnderTest": "$IP link add dev $DUMMY txqueuelen 500000 type dummy",
"expExitCode": "2",
"verifyCmd": "$IP -o link show",
"matchPattern": "^[0-9]+: $DUMMY",
"matchCount": "0",
"teardown": []
},
{
"id": "31ac",
"name": "Reject veth peer nest tx_queue_len above S16_MAX at create",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
[
"$IP link del dev $DEV1",
0,
1
]
],
"cmdUnderTest": "$IP link add dev $DEV1 type veth peer name $DEV0 txqueuelen 500000",
"expExitCode": "2",
"verifyCmd": "$IP -o link show",
"matchPattern": "^[0-9]+: $DEV1",
"matchCount": "0",
"teardown": []
},
{
"id": "b567",
"name": "Accept veth peer nest tx_queue_len within S16_MAX",
"category": [
"qdisc",
"pfifo_fast"
],
"plugins": {
"requires": "nsPlugin"
},
"setup": [
[
"$IP link del dev $DEV1",
0,
1
]
],
"cmdUnderTest": "$IP link add dev $DEV1 txqueuelen 100 type veth peer name $DEV0 txqueuelen 200",
"expExitCode": "0",
"verifyCmd": "$IP link show",
"matchPattern": "qlen (100|200)$",
"matchCount": "2",
"teardown": [
[
"$IP link del dev $DEV0",
0,
1
]
]
}
]