[BUG] l2tp: OOB read in l2tp_recv_common() for L2TPv2 sessions with 8-byte peer cookie

From: Guikun Yang

Date: Sun Oct 25 2026 - 13:35:41 EST


Hi Guillaume,

l2tp_recv_common() (net/l2tp/l2tp_core.c) reads L2TPv2 optional header
fields beyond the linear area guaranteed by the caller, for sessions
configured with an 8-byte peer cookie.

The UDP entry path only guarantees the first L2TP_HDR_SIZE_MAX (14)
bytes:

if (!pskb_may_pull(skb, L2TP_HDR_SIZE_MAX))
goto pass;

l2tp_recv_common(session, skb, ptr, optr, hdrflags, length);

Inside l2tp_recv_common(), for a L2TPv2 session with peer_cookie_len =
8, the cookie check consumes bytes [6, 14):

if (session->peer_cookie_len > 0) {
if (memcmp(ptr, &session->peer_cookie[0],
session->peer_cookie_len)) { ... discard }
ptr += session->peer_cookie_len; /* ptr = optr + 14 */
}

Then the sequence-number branch for L2TPv2 reads at ptr:

if (hdrflags & L2TP_HDRFLAG_S) {
L2TP_SKB_CB(skb)->ns = ntohs(*(__be16 *)ptr); /* [14, 16) OOB */
ptr += 2;
ptr += 2;
}

With a minimal 14-byte L2TPv2 packet (6-byte fixed header + 8-byte
cookie, no payload, L2TP_HDRFLAG_S set), skb->len is exactly 14 after
__skb_pull(udphdr), so the read at [14, 16) is past the end of the
linear area — a 2-byte slab-out-of-bounds read (KASAN catchable). The
subsequent `ptr += 2 + offset` handling for L2TP_HDRFLAG_O can read
even further out ([18, 20) with the same setup).

Trigger conditions:
- an L2TPv2 session with peer_cookie_len == 8 (admin-configured),
- a UDP packet whose L2TP payload is exactly 14 bytes, with
L2TP_HDRFLAG_S set and the correct cookie.

The peer address is not verified on the UDP path (l2tp_v2_session_get()
matches only on tunnel_id/session_id), so any host that knows the
cookie — i.e. a peer of the tunnel, or anyone when no cookie is
configured (in which case the OOB read does not trigger, since the
pointer stays at 6 and the seq read lands at [6, 10)) — can send such a
packet.

This is the L2TPv2 counterpart of the L2TPv3 optional-fields OOB fixed
in 2019 by commit 4522a70db7aa ("l2tp: fix reading optional fields of
L2TPv3"), which added l2tp_v3_ensure_opt_in_linear(). The commit
message of that fix explicitly noted that fixing L2TPv2 as well would
require refactoring l2tp_recv_common() and was deferred:

To fix both L2TPv3 and L2TPv2, we'd better refactor
l2tp_recv_common. It's complicated to do so.

L2TPv2 has remained unprotected since. The in-function
pskb_may_pull(skb, offset) at the end of l2tp_recv_common() runs only
after all the header reads have already happened, so it cannot help.

A minimal fix for the v2 path could ensure the header is fully linear
before parsing — e.g. in l2tp_udp_encap_recv(), after determining
version == L2TP_HDR_VER_2 and before calling l2tp_recv_common():

if (session->tunnel->version == L2TP_HDR_VER_2 &&
!pskb_may_pull(skb, 6 + session->peer_cookie_len +
(hdrflags & L2TP_HDRFLAG_S ? 4 : 0)))
goto invalid;

(or a full refactor pulling the cookie/seq handling into a
l2tp_v2_ensure_opt_in_linear() helper mirroring the v3 one).

Checked against 7.3.0-rc7 and linux-next (2026-10-24): neither carries a
fix; l2tp_v3_ensure_opt_in_linear() is still v3-only. I found no
report of this specific OOB on lore/marc.

Best,
Guikun Yang