[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 --- */