[PATCH v2] net/smc: release the internal TCP sock on IPPROTO_SMC socket creation failure

Chuyf26 <[email protected]>
Newsgroups org.kernel.vger.linux-rdma,org.kernel.vger.linux-s390,org.kernel.vger.netdev
Message-ID <20260813060456.b48HqF9m6NTfFXvq_PL9w_Ih493OogqAqxfAHAfU74A@z>
IPPROTO_SMC sockets create an internal TCP sock ("clcsock") from the
proto->init hook. When socket creation fails after proto->init has
run - e.g. a cgroup BPF program attached to BPF_CGROUP_INET_SOCK_CREATE
denies the socket - sk_common_release() only invokes sk_prot->destroy
if it is set, but neither smc_inet_prot nor smc_inet6_prot defines it,
and smc_destruct() returns early unless sk_state is SMC_CLOSED. As a
result, every failing socket(AF_INET, SOCK_STREAM, IPPROTO_SMC) call
leaks one tcp_sock, so an unprivileged task able to attach a deny-all
BPF_CGROUP_INET_SOCK_CREATE program to its own cgroup can grow kernel
memory unboundedly.

Add a .destroy hook to both protos that releases the clcsock via
smc_clcsock_release(), which is safe here because it skips a NULL
clcsock under clcsock_release_lock. Also initialize clcsock to NULL
when setting the sock up: the smc_sock slab is SLAB_TYPESAFE_BY_RCU,
so recycled objects are not zeroed.

Fixes: d25a92ccae6b ("net/smc: Introduce IPPROTO_SMC")
Reported-by: Abaci <[email protected]>
Assisted-by: abaci:qwen3.8-max
Signed-off-by: Chuyf26 <[email protected]>
---
 net/smc/smc_inet.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/net/smc/smc_inet.c b/net/smc/smc_inet.c
index a94084b..b94a194 100644
--- a/net/smc/smc_inet.c
+++ b/net/smc/smc_inet.c
@@ -15,13 +15,16 @@
 
 #include "smc_inet.h"
 #include "smc.h"
+#include "smc_close.h"
 
 static int smc_inet_init_sock(struct sock *sk);
+static void smc_inet_destroy_sock(struct sock *sk);
 
 static struct proto smc_inet_prot = {
 	.name		= "INET_SMC",
 	.owner		= THIS_MODULE,
 	.init		= smc_inet_init_sock,
+	.destroy	= smc_inet_destroy_sock,
 	.hash		= smc_hash_sk,
 	.unhash		= smc_unhash_sk,
 	.release_cb	= smc_release_cb,
@@ -68,6 +71,7 @@ static struct proto smc_inet6_prot = {
 	.name		= "INET6_SMC",
 	.owner		= THIS_MODULE,
 	.init		= smc_inet_init_sock,
+	.destroy	= smc_inet_destroy_sock,
 	.hash		= smc_hash_sk,
 	.unhash		= smc_unhash_sk,
 	.release_cb	= smc_release_cb,
@@ -109,6 +113,14 @@ static struct inet_protosw smc_inet6_protosw = {
 static int smc_inet_init_sock(struct sock *sk)
 {
 	struct net *net = sock_net(sk);
+	struct smc_sock *smc = smc_sk(sk);
+
+	/*
+	 * The smc_sock slab is SLAB_TYPESAFE_BY_RCU and recycled objects
+	 * are not zeroed. .destroy may run even if .init never completed,
+	 * so make sure smc_clcsock_release() sees a valid clcsock.
+	 */
+	smc->clcsock = NULL;
 
 	/* init common smc sock */
 	smc_sk_init(net, sk, IPPROTO_SMC);
@@ -116,6 +128,17 @@ static int smc_inet_init_sock(struct sock *sk)
 	return smc_create_clcsk(net, sk, sk->sk_family);
 }
 
+static void smc_inet_destroy_sock(struct sock *sk)
+{
+	/*
+	 * If inet_create()/inet6_create() fail after .init has created the
+	 * internal TCP sock (e.g. rejected by a cgroup BPF program),
+	 * sk_common_release() ends up here. Release the TCP sock, otherwise
+	 * it leaks on every failed IPPROTO_SMC socket() call.
+	 */
+	smc_clcsock_release(smc_sk(sk));
+}
+
 int __init smc_inet_init(void)
 {
 	int rc;
-- 
2.43.5

Thanks for the review. Changes since v1:

- Trimmed the commit message as suggested; the code is unchanged.
- Kept the reproducer and measurements below this separator, in
  case anyone wants to give the leak a spin or double-check the fix.

Reproducer (no clang/bpftool needed, hand-assembled BPF via bpf(2));
run as root on a cgroup-v2 system, needs CAP_BPF and CONFIG_CGROUP_BPF:

  $ gcc -O2 -o smc_leak smc_leak.c
  $ ./smc_leak 20000

It attaches a deny-all BPF_CGROUP_INET_SOCK_CREATE program to a fresh
cgroup, enters it and loops socket(AF_INET, SOCK_STREAM, IPPROTO_SMC).
Before this patch every call fails with EPERM and the slabinfo "TCP"
active_objs count grows by ~1 per call (20004 for 20000 iterations,
never shrinking); with this patch the count stays flat.

/* --- smc_leak.c --- */
/*
 * IPPROTO_SMC socket-creation leak reproducer (no clang/bpftool needed).
 *
 * 1. creates a cgroupv2 subgroup, loads a hand-assembled deny-all
 *    BPF_CGROUP_INET_SOCK_CREATE prog via bpf(2), attaches it;
 * 2. moves itself into that cgroup;
 * 3. loops socket(AF_INET, SOCK_STREAM, IPPROTO_SMC);
 * 4. prints slabinfo "TCP" delta and kmemleak report.
 *
 * Run as root on a cgroup-v2 system:
 *   ./smc_leak 20000
 * Expected on buggy kernel: every socket() fails with EPERM and leaks one
 * TCP sock per call (slabinfo "TCP" active_objs grows by ~1 per iteration;
 * with CONFIG_DEBUG_KMEMLEAK, unreferenced tcp_sock objects are reported).
 * On a fixed kernel: no growth.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/bpf.h>
#include <stdint.h>

#ifndef IPPROTO_SMC
#define IPPROTO_SMC 256
#endif

#define BPF_RAW(CODE, DST, SRC, OFF, IMM) ((struct bpf_insn){CODE, DST, SRC, OFF, IMM})

static int sys_bpf(int cmd, union bpf_attr *attr, unsigned int size)
{
	return syscall(__NR_bpf, cmd, attr, size);
}

static int load_deny_prog(void)
{
	/* r0 = 0 (deny); exit */
	struct bpf_insn insns[] = {
		BPF_RAW(BPF_ALU64 | BPF_MOV | BPF_K, BPF_REG_0, 0, 0, 0),
		BPF_RAW(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
	};
	union bpf_attr attr;
	memset(&attr, 0, sizeof(attr));
	attr.prog_type = BPF_PROG_TYPE_CGROUP_SOCK;
	attr.insns = (uint64_t)(unsigned long)insns;
	attr.insn_cnt = 2;
	attr.license = (uint64_t)(unsigned long)"GPL";
	int fd = sys_bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
	if (fd < 0)
		perror("BPF_PROG_LOAD");
	return fd;
}

static int attach_prog(int prog_fd, const char *cgpath)
{
	int cg_fd = open(cgpath, O_RDONLY | O_DIRECTORY);
	if (cg_fd < 0) {
		perror("open cgroup");
		return -1;
	}
	union bpf_attr attr;
	memset(&attr, 0, sizeof(attr));
	attr.link_create.prog_fd = prog_fd;
	attr.link_create.target_fd = cg_fd;
	attr.link_create.attach_type = BPF_CGROUP_INET_SOCK_CREATE;
	int fd = sys_bpf(BPF_LINK_CREATE, &attr, sizeof(attr));
	if (fd < 0) {
		perror("BPF_LINK_CREATE");
		close(cg_fd);
		return -1;
	}
	return fd;
}

static long tcp_active_objs(void)
{
	FILE *f = fopen("/proc/slabinfo", "r");
	char line[256];
	long objs = -1;

	if (!f)
		return -1;
	while (fgets(line, sizeof(line), f)) {
		char name[64];
		long active;
		if (sscanf(line, "%63s %ld", name, &active) == 2 &&
		    !strcmp(name, "TCP")) {
			objs = active;
			break;
		}
	}
	fclose(f);
	return objs;
}

static void kmemleak_scan(void)
{
	mkdir("/sys/kernel/debug", 0755);
	mount("debugfs", "/sys/kernel/debug", "debugfs", 0, NULL);
	FILE *f = fopen("/sys/kernel/debug/kmemleak", "w");
	if (f) {
		fputs("scan", f);
		fclose(f);
		sleep(3);
		system("grep -c 'unreferenced object' /sys/kernel/debug/kmemleak 2>/dev/null | sed 's/^/kmemleak unreferenced objects: /'");
		system("head -60 /sys/kernel/debug/kmemleak 2>/dev/null");
	} else {
		printf("(kmemleak unavailable: %s)\n", strerror(errno));
	}
}

int main(int argc, char **argv)
{
	const char *name = argc > 1 ? argv[1] : "smctest";
	long iters = argc > 2 ? atol(argv[2]) : 20000;
	char cgpath[256], path[280];
	int prog_fd, link_fd;

	if (access("/sys/fs/cgroup/cgroup.controllers", F_OK)) {
		fprintf(stderr, "cgroup v2 required\n");
		return 1;
	}

	snprintf(cgpath, sizeof(cgpath), "/sys/fs/cgroup/%s", name);
	mkdir(cgpath, 0755);

	prog_fd = load_deny_prog();
	if (prog_fd < 0)
		return 1;
	link_fd = attach_prog(prog_fd, cgpath);
	if (link_fd == -1)
		return 1;

	snprintf(path, sizeof(path), "%s/cgroup.procs", cgpath);
	FILE *f = fopen(path, "w");
	if (!f) {
		perror("open cgroup.procs");
		return 1;
	}
	fprintf(f, "%d", getpid());
	fclose(f);

	long before = tcp_active_objs();
	printf("TCP slab objs before: %ld\n", before);

	long denied = 0, ok = 0, other = 0;
	for (long i = 0; i < iters; i++) {
		int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SMC);
		if (fd >= 0) {
			close(fd);
			ok++;
		} else if (errno == EPERM) {
			denied++;
		} else {
			if (!other)
				printf("socket error: %s\n", strerror(errno));
			other++;
			break;
		}
	}
	long after = tcp_active_objs();
	printf("iterations=%ld denied(EPERM)=%ld ok=%ld other=%ld\n",
	       iters, denied, ok, other);
	printf("TCP slab objs after: %ld  (delta %+ld)\n", after, after - before);
	if (denied && after - before > denied / 2)
		printf("=> LEAK CONFIRMED: ~%ld TCP socks leaked on error path\n",
		       after - before);

	kmemleak_scan();

	/* leave cgroup so it can be removed */
	f = fopen("/sys/fs/cgroup/cgroup.procs", "w");
	if (f) {
		fprintf(f, "%d", getpid());
		fclose(f);
	}
	if (link_fd >= 0)
		close(link_fd);
	rmdir(cgpath);
	return 0;
}
/* --- end smc_leak.c --- */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.