[PATCH] SUNRPC: fix netns use-after-free in write_gssp()

From: Zijing Yin

Date: Mon Sep 07 2026 - 05:14:16 EST


While fuzzing with a customized syzkaller, I hit a refcount warning in
xprt_alloc(), reached from a write to /proc/net/rpc/use-gss-proxy:

refcount_t: addition on 0; use-after-free.
WARNING: lib/refcount.c:25 at refcount_warn_saturate+0xf8/0x120
CPU: 0 PID: 10404 Comm: syz.0.17 Not tainted 7.3.0-rc1-00096-gcfebfd3db73d
Call Trace:
<TASK>
xprt_alloc+0x83f/0x9d0
xs_setup_xprt+0xaf/0x3c0
xs_setup_local+0x47/0x7f0
xprt_create_transport+0x16c/0x730
rpc_create+0x38e/0x7f0
gssp_rpc_create+0xe2/0x180
set_gssp_clnt+0xba/0x1b0
write_gssp+0x200/0x310
proc_reg_write+0x240/0x330
vfs_write+0x2aa/0x1050
ksys_write+0x12a/0x250
do_syscall_64+0x117/0x750
entry_SYSCALL_64_after_hwframe+0x77/0x7f
</TASK>

create_use_gss_proxy_proc_entry() stores the struct net pointer as the
proc entry's private data without taking a reference on it, and an open
file descriptor does not pin the namespace either, as procfs only pins
the proc_dir_entry. write_gssp() reads that pointer back with
pde_data() and hands it to set_gssp_clnt(), which reaches xprt_init():

xprt->xprt_net = get_net_track(net, &xprt->ns_tracker, GFP_KERNEL);

get_net_track() -> get_net() -> refcount_inc() is unconditional, so
rpc_create() assumes its caller holds a reference on args.net.
write_gssp() does not.

Dropping the last ns.count reference does not make the file go away.
__put_net() only queues cleanup_net() on a workqueue, and the
remove_proc_entry() that fences off further writes runs from the
rpcsec_gss pernet .exit method, that is, from inside cleanup_net(). A
write landing between those two points increments a refcount that is
already zero. cleanup_net() does not re-read ns.count, so the namespace
is freed anyway and the rpc_xprt -- along with the AF_LOCAL socket
opened for it -- is left pointing at freed memory. The resurrected
count is visible to the rest of that teardown too: with
CONFIG_IPV6_MROUTE the same cleanup_net() worker then trips
!mr_can_free_table() in ip6mr_free_table(), which tests check_net() on
the namespace it is freeing.

It reduces to opening /proc/net/rpc/use-gss-proxy inside a new network
namespace, calling setns() back to the original one to drop the last
reference, and then writing "1" to the still-open descriptor.

Take the reference in write_gssp() itself and refuse the write when the
namespace is already gone. This is what procfs does for every other
/proc/net file: get_proc_net() is maybe_get_net(PDE_NET(PDE(inode)))
and seq_open_net() returns -ENXIO when it fails. PDE_NET() cannot be
reused here because the parent directory /proc/net/rpc carries no
namespace pointer. Adding a .pre_exit to rpcsec_gss_net_ops so the
entry is removed earlier would only narrow the window, since pre_exit
also runs from cleanup_net().

Live namespaces are unaffected: a write still returns the error from
set_gssp_clnt(), a write of "2" still returns -EINVAL, and reads are
untouched.

Fixes: 030d794bf498 ("SUNRPC: Use gssproxy upcall for server RPCGSS authentication.")
Signed-off-by: Zijing Yin <yzjaurora@xxxxxxxxx>
---
Applies to nfsd-testing (8ba9d2d76000), v7.3-rc1, nfsd-next and nfsd-fixes;
svcauth_gss.c is identical in all of them. Tested with KASAN and
CONFIG_NET_NS_REFCNT_TRACKER: unpatched, the reproducer below warns on every
boot; patched, it returns -ENXIO over 12704 iterations with no splat and no
ref_tracker report.

Reproducer (cc -static; needs CONFIG_SUNRPC_GSS=y, run as root):

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
int host = open("/proc/self/ns/net", O_RDONLY);
int fd;

if (host < 0 || unshare(CLONE_NEWNET))
return 1;
fd = open("/proc/self/net/rpc/use-gss-proxy", O_WRONLY);
if (fd < 0 || setns(host, CLONE_NEWNET))
return 1;
if (write(fd, "1", 1) < 0)
printf("write: %s\n", strerror(errno));
return 0;
}

net/sunrpc/auth_gss/svcauth_gss.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/net/sunrpc/auth_gss/svcauth_gss.c b/net/sunrpc/auth_gss/svcauth_gss.c
index 967e9d53080d..8e0b6b17c81d 100644
--- a/net/sunrpc/auth_gss/svcauth_gss.c
+++ b/net/sunrpc/auth_gss/svcauth_gss.c
@@ -1420,10 +1420,19 @@ static ssize_t write_gssp(struct file *file, const char __user *buf,
return res;
if (i != 1)
return -EINVAL;
+
+ /*
+ * The proc entry does not hold a reference on @net, and neither
+ * does an open file descriptor, so @net can already be dying.
+ * rpc_create() below takes a reference unconditionally.
+ */
+ if (!maybe_get_net(net))
+ return -ENXIO;
+
res = set_gssp_clnt(net);
- if (res)
- return res;
- res = set_gss_proxy(net, 1);
+ if (!res)
+ res = set_gss_proxy(net, 1);
+ put_net(net);
if (res)
return res;
return count;

base-commit: 8ba9d2d760007b15f8b4e8a812c9c0dfd66a2763
--
2.43.0