* [BUG] NFSD: write_unlock_fs() has the same pre-shutdown-check UAF that 92ea163c773c just fixed for the netlink command
@ 2026-08-01 19:32 George Chapman
2026-08-02 1:50 ` Chuck Lever
0 siblings, 1 reply; 2+ messages in thread
From: George Chapman @ 2026-08-01 19:32 UTC (permalink / raw)
To: linux-nfs; +Cc: Chuck Lever, Jeff Layton
[-- Attachment #1.1: Type: text/plain, Size: 4948 bytes --]
Hi all,
Bit of context, my name is George Chapman and I'm a cyber security
researcher from the UK.
I'm sure you get a lot of these but recently I've been exploring
AI-assisted tooling as part of my own security research, using it alongside
manual code review to look for bugs on local Linux builds.
I've recently found something and want to raise it with you please,
essentially this report is a product of that process, an LLM helped me
trace through the code under my direction, with everything checked by hand
before sending.
I want to mention this upfront in case it's useful to know, and happy
to adjust how I send these if there's a channel you'd prefer.
After reading through recent NFSD fixes and came across *92ea163c773c*
("NFSD: Prevent post-shutdown use-after-free in
NFSD_CMD_UNLOCK_FILESYSTEM"), which fixes nfsd_nl_unlock_filesystem_doit()
calling nfsd4_cancel_copy_by_sb() before nfsd_mutex is held and before
nn->nfsd_serv is confirmed set.
The commit message says this directly: "the same pre-mutex ordering the
procfs unlock_filesystem path carried" That's describing write_unlock_fs()
in fs/nfsd/nfsctl.c, the handler behind /proc/fs/nfsd/unlock_filesystem
(same operation, older interface).
Checking the current tree, the fix's diff only
touched nfsd_nl_unlock_filesystem_doit(); write_unlock_fs() itself, a few
hundred lines above it in the same file, still has exactly the ordering the
commit describes as unsafe:
fs/nfsd/nfsctl.c:299 (mainline as of 0c452fbdf413, 2026-07-24):
error = kern_path(fo_path, 0, &path);
if (error)
return error;
...
nfsd4_cancel_copy_by_sb(netns(file), path.dentry->d_sb);
error = nlmsvc_unlock_all_by_sb(path.dentry->d_sb);
mutex_lock(&nfsd_mutex);
nn = net_generic(netns(file), nfsd_net_id);
if (nn->nfsd_serv)
nfsd4_revoke_states(nn, path.dentry->d_sb);
else
error = -EINVAL;
mutex_unlock(&nfsd_mutex);
nfsd4_cancel_copy_by_sb() walks nn->conf_id_hashtbl unconditionally, with
no lock held and no check that NFSD is running:
spin_lock(&nn->client_lock);
for (idhashval = 0; idhashval < CLIENT_HASH_SIZE; idhashval++) {
struct list_head *head = &nn->conf_id_hashtbl[idhashval];
list_for_each_entry(clp, head, cl_idhash) {
spin_lock(&clp->async_lock);
...
*nfs4_state_destroy_net() (fs/nfsd/nfs4state.c:9184,* called from
*nfs4_state_shutdown_net()
on server stop)* does "kfree(nn->conf_id_hashtbl);" as part of the same
shutdown sequence which sets *nn->nfsd_serv* back to NULL.
So after a stop, *conf_id_hashtbl *is dangling while *nfsd_serv == NULL *sits
right there unchecked a few lines later in write_unlock_fs().
Writing to /proc/fs/nfsd/unlock_filesystem after stopping NFSD in that net
namespace looks like it would walk freed memory as a list and take
a spinlock on whatever bogus pointer falls out of it, the
same use-after-free shape as the bug 92ea163c773c just fixed, just reached
through the older procfs interface instead of the netlink one.
I reproduced this locally under a KASAN kernel built from this tree
(CONFIG_NFSD=y, no export or NFS client needed): mount nfsd, start
threads, stop threads, then write "/" to unlock_filesystem. Crashed on the
first attempt, no timing needed since the hash table is freed synchronously
in the thread-stop path:
Oops: general protection fault, probably for non-canonical address
0xdffffc0000000000: 0000 [#1] KASAN
KASAN: null-ptr-deref in range [0x0000000000000000-0x0000000000000007]
RIP: 0010:nfsd4_cancel_copy_by_sb+0x1ca/0x9c0
RAX: 0000000000000000 RBX: 0000000000000000 ...
Call Trace:
write_unlock_fs+0x20c/0x760
nfsctl_transaction_write+0xd5/0x140
vfs_write+0x24e/0x1050
ksys_write+0xfe/0x1e0
RAX/RBX reading 0 in the [0x0-0x7] range matches head
= &nn->conf_id_hashtbl[idhashval] off a freed (and zeroed-on-free on this
debug config) array pointer, exactly the mechanism above.
This needs local write access to that file (owner-only, effectively root or
a process specifically granted it), same access level as the already-fixed
netlink command required, so it's not remotely triggerable as far as I can
tell.
This hasn't been through upstream triage, just my own local repro and
source reading.
I reproduced it again today (2026-08-01) against current mainline
(2d2338c93da79b3bfe4b6099a931d9468d539952), same crash signature,
same RIP, same call chain, confirming this is still open 8 days
after 92ea163c773c fixed the sibling and called out this exact ordering.
Fuller writeup attached
(*advisory-linux-nfsd-write-unlock-fs-cancel-copy-uaf.md*), including
a rough sketch of a fix mirroring 92ea163c773c's shape, offered as a
starting point rather than a tested patch.
I know this isn't a finished exploit, and I'm happy to help in whatever way
would be useful from here.
Thank you for your time, and for maintaining this code.
Best wishes,
George C
[-- Attachment #1.2: Type: text/html, Size: 5871 bytes --]
[-- Attachment #2: advisory-linux-nfsd-write-unlock-fs-cancel-copy-uaf.md --]
[-- Type: text/markdown, Size: 9045 bytes --]
# Security Advisory: Linux kernel NFSD: `write_unlock_fs()` calls `nfsd4_cancel_copy_by_sb()` before checking `nn->nfsd_serv`, use-after-free on a stopped server
- **Product:** Linux kernel, `fs/nfsd/nfsctl.c` (`/proc/fs/nfsd/unlock_filesystem` procfs write handler)
- **Tested against commit:** `0c452fbdf41374ff418cb069e59d141eb73f374a` (mainline, 2026-07-24). `fs/nfsd/nfsctl.c` itself last touched by `92ea163c773c` (2026-07-05); no commits to the file since.
## Status up front
Live-reproduced under a KASAN-instrumented QEMU kernel built from current mainline (see "Live reproduction" below). It is submitted with high confidence both because the crash was obtained directly and because the maintainer's own fix commit describes this exact code path as sharing the bug it fixed, in its own commit message, without the fix itself touching it. This has not gone through upstream triage or maintainer review; the analysis and reproduction below are offered as-is, on the strength of the local repro and source reading, not as a maintainer-confirmed finding.
## Live reproduction
Built a kernel from this project's local mainline checkout with `CONFIG_NFSD=y`, `CONFIG_NFSD_V4=y`, KASAN (generic, inline) already enabled in the working config, booted under QEMU/TCG. No NFS export or NFS client is needed; the sequence below is entirely local to the machine running the kernel:
```
mount -t nfsd nfsd /proc/fs/nfsd # if not already mounted
echo 4 > /proc/fs/nfsd/threads # start nfsd
echo 0 > /proc/fs/nfsd/threads # stop nfsd; frees nn->conf_id_hashtbl
printf '/\n' > /proc/fs/nfsd/unlock_filesystem
```
This reproduced on the first attempt, with no delay between the stop and the write, which is consistent with the bug being deterministic rather than a narrow timing race: the hash table is freed synchronously in the thread-stop path, so any write to `unlock_filesystem` afterward hits it.
Panic (KASAN, general protection fault presenting as a null-pointer dereference, consistent with this being an `init_on_free`-style debug kernel where freed slab memory reads back as zero rather than arbitrary poison bytes):
```
Oops: general protection fault, probably for non-canonical address 0xdffffc0000000000: 0000 [#1] KASAN
KASAN: null-ptr-deref in range [0x0000000000000000-0x0000000000000007]
CPU: 0 UID: 0 PID: 250 Comm: bash Not tainted 7.2.0-rc4+ #1 PREEMPTLAZY
RIP: 0010:nfsd4_cancel_copy_by_sb+0x1ca/0x9c0
RAX: 0000000000000000 RBX: 0000000000000000 ...
Call Trace:
<TASK>
write_unlock_fs+0x20c/0x760
nfsctl_transaction_write+0xd5/0x140
vfs_write+0x24e/0x1050
ksys_write+0xfe/0x1e0
__x64_sys_write+0x76/0xb0
do_syscall_64+0x91/0x4f0
entry_SYSCALL_64_after_hwframe+0x4b/0x53
```
`RAX`/`RBX` reading `0` and the crash address falling in `[0x0-0x7]` matches `head = &nn->conf_id_hashtbl[idhashval]` being computed from a freed (and, on this debug config, zeroed-on-free) array pointer, exactly the mechanism described below. The trace names `write_unlock_fs` and `nfsd4_cancel_copy_by_sb` directly, with no other code in between, which is about as direct a confirmation of the traced call chain as a crash trace can give.
## Finding: `write_unlock_fs()` races NFSD shutdown, use-after-free of `nn->conf_id_hashtbl`
- **Location:** `fs/nfsd/nfsctl.c:268-310`, function `write_unlock_fs()` (the handler behind `/proc/fs/nfsd/unlock_filesystem`, registered at `fs/nfsd/nfsctl.c:1302-1303` with mode `S_IWUSR|S_IRUSR`, i.e. writable only by the file's owner, normally root).
- **CWE:** CWE-416 (Use After Free), CWE-362 (race condition / TOCTOU).
```c
static ssize_t write_unlock_fs(struct file *file, char *buf, size_t size)
{
struct path path;
char *fo_path;
int error;
struct nfsd_net *nn;
...
error = kern_path(fo_path, 0, &path);
if (error)
return error;
...
nfsd4_cancel_copy_by_sb(netns(file), path.dentry->d_sb);
error = nlmsvc_unlock_all_by_sb(path.dentry->d_sb);
mutex_lock(&nfsd_mutex);
nn = net_generic(netns(file), nfsd_net_id);
if (nn->nfsd_serv)
nfsd4_revoke_states(nn, path.dentry->d_sb);
else
error = -EINVAL;
mutex_unlock(&nfsd_mutex);
path_put(&path);
return error;
}
```
`nfsd4_cancel_copy_by_sb()` (`fs/nfsd/nfs4proc.c:1566`) is called here with no lock held and with no check that NFSD is even running. It walks the full client ID hash table:
```c
void nfsd4_cancel_copy_by_sb(struct net *net, struct super_block *sb)
{
struct nfsd_net *nn = net_generic(net, nfsd_net_id);
...
spin_lock(&nn->client_lock);
for (idhashval = 0; idhashval < CLIENT_HASH_SIZE; idhashval++) {
struct list_head *head = &nn->conf_id_hashtbl[idhashval];
list_for_each_entry(clp, head, cl_idhash) {
spin_lock(&clp->async_lock);
...
```
When NFSD is stopped in a net namespace, `nfs4_state_shutdown_net()` calls `nfs4_state_destroy_net()` (`fs/nfsd/nfs4state.c:9162-9186`), which frees the array the hash table lives in and nulls the "server running" flag as part of the same shutdown sequence:
```c
static void
nfs4_state_destroy_net(struct net *net)
{
...
kfree(nn->sessionid_hashtbl);
kfree(nn->unconf_id_hashtbl);
kfree(nn->conf_id_hashtbl); /* fs/nfsd/nfs4state.c:9184 */
put_net(net);
}
```
`nn->nfsd_serv` is set back to `NULL` in the same shutdown path (`fs/nfsd/nfssvc.c:510`, `fs/nfsd/nfsctl.c:2446`). After shutdown, `nn->conf_id_hashtbl` is a dangling pointer into freed slab memory, and `nn->nfsd_serv == NULL` is the documented, already-used signal that it is unsafe to touch NFSv4 state.
`write_unlock_fs()` never checks `nn->nfsd_serv` before calling `nfsd4_cancel_copy_by_sb()`. If a caller with write access to `/proc/fs/nfsd/unlock_filesystem` (root, or any process granted that specific file's write permission) issues the write after NFSD has been stopped in that net namespace, `nfsd4_cancel_copy_by_sb()` reads `nn->conf_id_hashtbl[idhashval]` out of freed memory, interprets whatever bytes are there as a `struct list_head`, and `list_for_each_entry(clp, head, cl_idhash)` walks it, taking `spin_lock(&clp->async_lock)` on a bogus `clp` pointer derived from freed/reused memory. This is a straightforward slab use-after-free with attacker-influenced timing (stop the server, then write the file).
## Why this isn't a duplicate: the maintainer's own fix names this exact path
Commit `92ea163c773c` ("NFSD: Prevent post-shutdown use-after-free in NFSD_CMD_UNLOCK_FILESYSTEM", 2026-07-05, `Fixes: 327c5168eff2`) fixed the identical bug in the netlink-based sibling command, `nfsd_nl_unlock_filesystem_doit()`. Before the fix, that function had exactly this shape: call `nfsd4_cancel_copy_by_sb()` before taking `nfsd_mutex` or checking `nn->nfsd_serv`. The fix moved the `nfsd4_cancel_copy_by_sb()` call to inside the mutex-held, `nn->nfsd_serv`-confirmed block.
The commit message states plainly:
> "The NFSD_CMD_UNLOCK_FILESYSTEM netlink command runs nfsd4_cancel_copy_by_sb() before nfsd_mutex is held and before nn->nfsd_serv is confirmed set, **the same pre-mutex ordering the procfs unlock_filesystem path carried**."
That sentence is describing `write_unlock_fs()`, the procfs handler for the same operation (`/proc/fs/nfsd/unlock_filesystem`, registered at `fs/nfsd/nfsctl.c:1302-1303`, both handlers ultimately reachable through `nfsd_fs.c`'s `nfsdfs` and the netlink family for the same logical command). The fix's diff only touches `fs/nfsd/nfsctl.c`'s `nfsd_nl_unlock_filesystem_doit()`; `write_unlock_fs()`, a few hundred lines above it in the same file, is untouched and still has the ordering the commit message describes as unsafe.
Confirmed via `git log --oneline 92ea163c773c..HEAD -- fs/nfsd/nfsctl.c`: empty output, no commits to this file since the fix landed on 2026-07-05, through current HEAD (`0c452fbdf413`, 2026-07-24).
## Suggested fix
Mirror the shape of the already-applied fix: move `nfsd4_cancel_copy_by_sb()` inside the `mutex_lock(&nfsd_mutex)` / `if (nn->nfsd_serv)` block, after `nn` is resolved and confirmed. Rough sketch, not tested:
```c
error = nlmsvc_unlock_all_by_sb(path.dentry->d_sb);
mutex_lock(&nfsd_mutex);
nn = net_generic(netns(file), nfsd_net_id);
if (nn->nfsd_serv) {
nfsd4_cancel_copy_by_sb(netns(file), path.dentry->d_sb);
nfsd4_revoke_states(nn, path.dentry->d_sb);
} else {
error = -EINVAL;
}
mutex_unlock(&nfsd_mutex);
```
This is offered as a starting point for the maintainers to adapt, not a drop-in patch; there may be reasons specific to this older procfs interface that call for something slightly different from the netlink fix's shape.
## Scope and impact
This requires local write access to `/proc/fs/nfsd/unlock_filesystem` (owner-only, effectively root or a process specifically granted that capability) and a stopped NFSD server in the relevant net namespace, matching the access level the already-fixed netlink sibling required. Not remotely triggerable. Impact is a kernel slab use-after-free, plausibly a crash and potentially further corruption depending on what reoccupies the freed `conf_id_hashtbl` allocation by the time it's walked.
^ permalink raw reply [flat|nested] 2+ messages in thread* Re: [BUG] NFSD: write_unlock_fs() has the same pre-shutdown-check UAF that 92ea163c773c just fixed for the netlink command
2026-08-01 19:32 [BUG] NFSD: write_unlock_fs() has the same pre-shutdown-check UAF that 92ea163c773c just fixed for the netlink command George Chapman
@ 2026-08-02 1:50 ` Chuck Lever
0 siblings, 0 replies; 2+ messages in thread
From: Chuck Lever @ 2026-08-02 1:50 UTC (permalink / raw)
To: George Chapman; +Cc: Jeff Layton, linux-nfs
Hello George -
On Sat, Aug 1, 2026, at 3:32 PM, George Chapman wrote:
> Hi all,
>
> Bit of context, my name is George Chapman and I'm a cyber security
> researcher from the UK.
> I'm sure you get a lot of these but recently I've been exploring
> AI-assisted tooling as part of my own security research, using it alongside
> manual code review to look for bugs on local Linux builds.
>
> I've recently found something and want to raise it with you please,
> essentially this report is a product of that process, an LLM helped me
> trace through the code under my direction, with everything checked by hand
> before sending.
Commit 208b023eef24 ("NFSD: Prevent post-shutdown use-after-free in
unlock_filesystem") is already queued in the nfsd-next branch.
You can search the linux-nfs@ list archive on lore.kernel.org as part
of your research to determine whether an issue like this has been
previously reported or fixed.
--
Chuck Lever
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-08-02 1:50 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-01 19:32 [BUG] NFSD: write_unlock_fs() has the same pre-shutdown-check UAF that 92ea163c773c just fixed for the netlink command George Chapman
2026-08-02 1:50 ` Chuck Lever
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.