From: Aohan Mei afs_finished_fs_probe() unconditionally re-adds server->probe_link to net->fs_probe_fast/slow when a probe round completes, while afs_server_destroyer() removes a server from those queues at most once: after afs_remove_server_from_cell() sets AFS_SERVER_FL_EXPIRED, every later destroyer pass early-returns and can never dequeue again. If the probe dispatcher dequeues a server and dispatches an async FS.GetCapabilities probe while the destroyer is between rb_erase() and its one-shot list_del_init() — a window the synchronous FS.GiveUpAllCallbacks RPC stretches to a full RPC round trip when AFS_SERVER_FL_MAY_HAVE_CB is set — the completion callback re-queues the already-expired server *after* that one-shot dequeue. Nothing ever removes the entry again; the call release then drops the last reference and the server is freed via call_rcu()/kfree() with probe_link still linked. The next afs_fs_probe_dispatcher() run reads fast->probed_at and writes probe_link/ref of the freed object: BUG: KASAN: slab-use-after-free in afs_fs_probe_dispatcher+0x520/0x640 Read of size 8 (fast->probed_at, fs/afs/fs_probe.c) refcount_t: addition on 0; use-after-free (afs_get_server) Skip the re-queue (and the probe-timer re-arm) when the server has AFS_SERVER_FL_EXPIRED set. The test sits inside the net->fs_lock seqlock critical section: the destroyer sets EXPIRED before its own seqlocked dequeue in program order, so any finished-probe critical section serialised after the destroyer's is guaranteed to observe the flag, while an earlier re-queue is still removed by the destroyer's own list_del_init(). Live servers are unaffected: an expired server is on the destruction path and must never be probed again. Fixes: f6cbb368bcb0 ("afs: Actively poll fileservers to maintain NAT or firewall openings") Reported-by: TencentOS Corvus AI Cc: stable@vger.kernel.org Assisted-by: CodeBuddy:Kimi-K3 Signed-off-by: Aohan Mei --- fs/afs/fs_probe.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/afs/fs_probe.c b/fs/afs/fs_probe.c --- a/fs/afs/fs_probe.c +++ b/fs/afs/fs_probe.c @@ -80,6 +80,16 @@ static void afs_finished_fs_probe(struct afs_net *net, struct afs_server *server bool responded = test_bit(AFS_ESTATE_RESPONDED, &estate->flags); write_seqlock(&net->fs_lock); + if (test_bit(AFS_SERVER_FL_EXPIRED, &server->flags)) { + /* The server is being destroyed and afs_server_destroyer() + * dequeues probe_link from the probe queues only once, so an + * expired server must not be re-queued here: the entry would + * outlive the server object and the next probe dispatcher + * run would touch freed memory. + */ + write_sequnlock(&net->fs_lock); + return; + } if (responded) { list_add_tail(&server->probe_link, &net->fs_probe_slow); } else { -- 2.43.7