[PATCH v2] ksmbd: refactor smb2_notify() to a blocking wait, matching smb2_lock()
"Gaƫl Blivet-Bailly" <[email protected]> Sat, 25 Jul 2026 12:26:44 +0200
| Newsgroups | org.kernel.vger.linux-cifs |
|---|---|
| Message-ID | <[email protected]> |
From: Gael Blivet <[email protected]> The previous design registered a synthetic work struct (in_work) directly on conn->async_requests and deferred the response to a workqueue -- a bespoke async/cancel implementation duplicating what setup_async_work(), release_async_work(), and smb2_send_interim_resp() already provide for smb2_lock()'s pending byte-range lock. Replace it with that same pattern: setup_async_work() on the calling work itself, registered on fp->blocked_works, woken by cancel or by the handle closing via the existing set_close_state_blocked_works(). This removes the synthetic work struct, the notify_pendings list and its close-time drain, and the deferred workqueue send, leaving smb2_notify() sharing the same async/cancel machinery as smb2_lock() instead of its own separate copy. The worker now blocks on ksmbd_wq for as long as the watch stays open, instead of returning immediately. This also makes the skeleton ready for a future event-delivery implementation on the same blocking wait. Suggested-by: ChenXiaoSong <[email protected]> Signed-off-by: Gael Blivet <[email protected]> Assisted-by: Claude:claude-sonnet-5 --- v1 -> v2: - Shortened the smb2_notify() kerneldoc to @work/Return only. - FileId sentinel path: use smb2_set_err_rsp() + STATUS_INVALID_PARAMETER instead of STATUS_PENDING/STATUS_NOTIFY_CLEANUP. - Removed dead owns_conn_ref field from struct ksmbd_work. - Removed the CHANGE_NOTIFY-wake comment in __ksmbd_close_fd(). fs/smb/server/ksmbd_work.c | 3 - fs/smb/server/ksmbd_work.h | 5 +- fs/smb/server/smb2pdu.c | 242 ++++++++----------------------------- fs/smb/server/vfs_cache.c | 48 -------- fs/smb/server/vfs_cache.h | 6 - 5 files changed, 51 insertions(+), 253 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index f35335307..097de59f8 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -57,7 +57,6 @@ struct ksmbd_work *ksmbd_alloc_work_struct(void) INIT_LIST_HEAD(&work->request_entry); INIT_LIST_HEAD(&work->async_request_entry); INIT_LIST_HEAD(&work->fp_entry); - INIT_LIST_HEAD(&work->notify_entry); INIT_LIST_HEAD(&work->aux_read_list); work->iov_alloc_cnt = ARRAY_SIZE(work->iov_inline); work->iov = work->iov_inline; @@ -87,8 +86,6 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) if (work->async_id) ksmbd_release_id(&work->conn->async_ida, work->async_id); - if (work->owns_conn_ref) - ksmbd_conn_put(work->conn); ksmbd_fd_put(work, work->request_open); kmem_cache_free(work_cache, work); } diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 52d0c4dee..9723f3cc9 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -91,8 +91,6 @@ struct ksmbd_work { bool compress_response:1; /* Is this SYNC or ASYNC ksmbd_work */ bool asynchronous:1; - /* Work owns a reference to @conn. */ - bool owns_conn_ref:1; bool need_invalidate_rkey:1; bool request_open_chseq_tracked:1; @@ -114,9 +112,8 @@ struct ksmbd_work { struct list_head request_entry; /* List head at conn->async_requests */ struct list_head async_request_entry; + /* List head at ksmbd_file->blocked_works */ struct list_head fp_entry; - /* List head at ksmbd_file->notify_pendings */ - struct list_head notify_entry; }; /** diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2038de315..8e5dfb079 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10857,86 +10857,26 @@ int smb2_oplock_break(struct ksmbd_work *work) return 0; } -/* - * Cancel handler for a deferred CHANGE_NOTIFY. Races against - * __ksmbd_close_fd()'s notify_pendings drain (vfs_cache.c), which can run - * concurrently on a different connection closing the same handle -- only - * one of the two may claim and free in_work, so both sides check - * list_empty() under fp->f_lock before touching it (list_del_init() - * leaves a node empty, so whichever side removes it first is the owner; - * the loser must not touch in_work again, since the winner may already be - * freeing it). - * - * smb2_cancel() holds conn->request_lock (a spinlock) for the entire - * time it walks conn->async_requests and calls this function -- so this - * runs with preemption disabled and must not sleep or re-acquire that - * same lock. release_async_work() does both (it takes conn->request_lock - * itself, and frees things that can involve sleeping paths), so calling - * it from here would self-deadlock the very thread processing the - * client's CANCEL command. ksmbd_conn_write() can also sleep (it takes - * conn's write mutex). So: do only the non-sleeping, no-relock cleanup - * inline here (the async_requests removal itself is safe without - * re-locking, since the caller already holds that lock), and defer the - * actual response send + work-struct free to a workqueue, matching the - * minimal, non-blocking style of the existing smb2_remove_blocked_lock() - * cancel_fn (which only wakes a waiter, never sends network data itself). - */ -struct notify_cancel_ctx { - struct work_struct work; - struct ksmbd_work *in_work; +struct ksmbd_notify_wait { + wait_queue_head_t wait; }; -static void smb2_notify_cancel_deferred(struct work_struct *w) -{ - struct notify_cancel_ctx *ctx = - container_of(w, struct notify_cancel_ctx, work); - struct ksmbd_work *in_work = ctx->in_work; - struct smb2_hdr *in_hdr; - - in_hdr = smb_get_msg(in_work->response_buf); - in_hdr->Status = STATUS_CANCELLED; - ksmbd_conn_write(in_work); - ksmbd_free_work_struct(in_work); - kfree(ctx); -} - -static void smb2_notify_cancel_fn(void **argv) +/* + * Cancel handler for a pending CHANGE_NOTIFY. Called either by + * smb2_cancel() (conn->request_lock held, work->state already set to + * KSMBD_WORK_CANCELLED by the caller) or by + * set_close_state_blocked_works() (vfs_cache.c, fp->f_lock held, + * work->state already set to KSMBD_WORK_CLOSED by the caller) -- both + * callers hold a spinlock across this call, so it must not sleep. + * wake_up() only wakes the waiter in smb2_notify(); it does not touch + * fp->blocked_works itself, matching smb2_remove_blocked_lock()'s same + * non-mutating style for the equivalent byte-range-lock wait. + */ +static void smb2_notify_cancel(void **argv) { - struct ksmbd_work *in_work = (struct ksmbd_work *)argv[0]; - struct ksmbd_file *fp = (struct ksmbd_file *)argv[1]; - struct ksmbd_conn *conn = in_work->conn; - struct notify_cancel_ctx *ctx; - bool claimed; - - spin_lock(&fp->f_lock); - claimed = !list_empty(&in_work->notify_entry); - if (claimed) - list_del_init(&in_work->notify_entry); - spin_unlock(&fp->f_lock); - - if (!claimed) - return; - - /* conn->request_lock is already held by the caller (smb2_cancel()). */ - list_del_init(&in_work->async_request_entry); - in_work->asynchronous = false; - in_work->cancel_fn = NULL; - kfree(in_work->cancel_argv); - in_work->cancel_argv = NULL; - if (in_work->async_id) { - ksmbd_release_id(&conn->async_ida, in_work->async_id); - in_work->async_id = 0; - } + struct ksmbd_notify_wait *notify_wait = (struct ksmbd_notify_wait *)argv[0]; - ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC); - if (!ctx) { - /* Can't defer the response -- free without sending one. */ - ksmbd_free_work_struct(in_work); - return; - } - ctx->in_work = in_work; - INIT_WORK(&ctx->work, smb2_notify_cancel_deferred); - schedule_work(&ctx->work); + wake_up(¬ify_wait->wait); } /** @@ -10949,9 +10889,10 @@ int smb2_notify(struct ksmbd_work *work) { struct smb2_change_notify_req *req; struct smb2_change_notify_rsp *rsp; - struct ksmbd_work *in_work; - struct smb2_hdr *in_hdr; + struct ksmbd_notify_wait notify_wait; struct ksmbd_file *fp; + void **argv; + int err; ksmbd_debug(SMB, "Received smb2 notify\n"); @@ -10967,56 +10908,17 @@ int smb2_notify(struct ksmbd_work *work) } /* - * macOS backupd sends CHANGE_NOTIFY with FileId=FFFF...FFFF (share-root - * sentinel) to watch for changes on the share root without holding an - * open handle. Respond STATUS_PENDING + STATUS_NOTIFY_CLEANUP immediately; - * without this, backupd aborts Time Machine setup on STATUS_FILE_CLOSED. + * macOS backupd sends CHANGE_NOTIFY with FileId=FFFF...FFFF + * (share-root sentinel) to watch for changes on the share root + * without holding an open handle. */ if (req->VolatileFileId == SMB2_NO_FID && req->PersistentFileId == SMB2_NO_FID) { - in_work = ksmbd_alloc_work_struct(); - if (!in_work || allocate_interim_rsp_buf(in_work)) { - if (in_work) - ksmbd_free_work_struct(in_work); - rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - smb2_set_err_rsp(work); - return 0; - } - if (setup_async_work(work, NULL, NULL)) { - ksmbd_free_work_struct(in_work); - rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - smb2_set_err_rsp(work); - return 0; - } - smb2_send_interim_resp(work, STATUS_PENDING); - in_work->conn = work->conn; - in_hdr = smb_get_msg(in_work->response_buf); - memcpy(in_hdr, ksmbd_resp_buf_next(work), - __SMB2_HEADER_STRUCTURE_SIZE); - in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; - in_hdr->Id.AsyncId = cpu_to_le64(work->async_id); - smb2_set_err_rsp(in_work); - in_hdr->Status = STATUS_NOTIFY_CLEANUP; - in_work->async_id = work->async_id; - work->async_id = 0; - release_async_work(work); - ksmbd_conn_write(in_work); - ksmbd_free_work_struct(in_work); - work->send_no_response = 1; + rsp->hdr.Status = STATUS_INVALID_PARAMETER; + smb2_set_err_rsp(work); return 0; } - /* - * KSMBD does not implement a real change-notification backend. - * Genuine SMB2 servers (and macOS smbfs) never complete a - * CHANGE_NOTIFY spontaneously: it is satisfied only by a real - * directory change, or with STATUS_NOTIFY_CLEANUP when the watched - * handle is closed. Completing it early (e.g. on a timer) makes - * Finder treat the cleanup as "directory changed" and re-enumerate - * the directory forever, leaving items unopenable. Returning - * STATUS_NOT_IMPLEMENTED here (like stock ksmbd) makes macOS smbfs - * hard-freeze on unmount, so this must stay deferred. - */ fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); if (!fp) { rsp->hdr.Status = STATUS_FILE_CLOSED; @@ -11024,96 +10926,52 @@ int smb2_notify(struct ksmbd_work *work) return 0; } - in_work = ksmbd_alloc_work_struct(); - if (!in_work || allocate_interim_rsp_buf(in_work)) { - if (in_work) - ksmbd_free_work_struct(in_work); - ksmbd_fd_put(work, fp); - rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - smb2_set_err_rsp(work); - return 0; - } - /* - * in_work is synthetic (not from the normal request-receiving - * pipeline), so it has no request_buf of its own. It gets registered - * into conn->async_requests below, and smb2_cancel() unconditionally - * computes smb_get_msg(iter->request_buf) for every entry in that - * list while searching for a match -- give it its own small buffer - * (not an alias of response_buf: ksmbd_free_work_struct() kvfree()s - * both separately, so aliasing them would double-free) so that stays - * a harmless read instead of a near-NULL dereference. - */ - in_work->request_buf = kzalloc(MAX_CIFS_SMALL_BUFFER_SIZE, KSMBD_DEFAULT_GFP); - if (!in_work->request_buf) { - ksmbd_free_work_struct(in_work); + argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP); + if (!argv) { ksmbd_fd_put(work, fp); rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; smb2_set_err_rsp(work); return 0; } + init_waitqueue_head(¬ify_wait.wait); + argv[0] = ¬ify_wait; - if (setup_async_work(work, NULL, NULL)) { - ksmbd_free_work_struct(in_work); + if (setup_async_work(work, smb2_notify_cancel, argv)) { + kfree(argv); ksmbd_fd_put(work, fp); rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; smb2_set_err_rsp(work); return 0; } - smb2_send_interim_resp(work, STATUS_PENDING); - - /* Keep the async IDA alive until the deferred work is released. */ - in_work->conn = ksmbd_conn_get(work->conn); - in_work->owns_conn_ref = true; - in_hdr = smb_get_msg(in_work->response_buf); - memcpy(in_hdr, ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); - in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; - in_hdr->Id.AsyncId = cpu_to_le64(work->async_id); - smb2_set_err_rsp(in_work); - in_hdr->Status = STATUS_NOTIFY_CLEANUP; + spin_lock(&fp->f_lock); + list_add_tail(&work->fp_entry, &fp->blocked_works); + spin_unlock(&fp->f_lock); - /* - * Transfer ownership of the async id to in_work; it stays reserved - * until in_work is freed after the deferred response is sent on - * close, so it can't be reused for an unrelated async response. - */ - in_work->async_id = work->async_id; - work->async_id = 0; - release_async_work(work); + smb2_send_interim_resp(work, STATUS_PENDING); - /* - * work itself is about to be recycled by the normal request-processing - * pipeline, so it can't stay the target of a future CANCEL -- register - * in_work instead, reusing the same async_id, so a client-sent CANCEL - * for this notify actually finds something to cancel instead of - * silently doing nothing until the handle eventually closes. - */ - in_work->asynchronous = true; - in_work->cancel_argv = kmalloc_array(2, sizeof(void *), KSMBD_DEFAULT_GFP); - if (in_work->cancel_argv) { - in_work->cancel_argv[0] = in_work; - in_work->cancel_argv[1] = fp; - in_work->cancel_fn = smb2_notify_cancel_fn; - } - - if (!ksmbd_conn_link_async_request(work->conn, in_work)) { - kfree(in_work->cancel_argv); - in_work->cancel_argv = NULL; - in_work->cancel_fn = NULL; - in_work->asynchronous = false; - ksmbd_fd_put(work, fp); - ksmbd_conn_write(in_work); - ksmbd_free_work_struct(in_work); - work->send_no_response = 1; - return 0; + err = wait_event_interruptible(notify_wait.wait, + READ_ONCE(work->state) != KSMBD_WORK_ACTIVE); + if (err && READ_ONCE(work->state) == KSMBD_WORK_ACTIVE) { + /* + * Woken by a signal, not a real cancel/close. There is no + * notification backend yet to report anything else against, + * so treat this the same as a client-side cancel. + */ + WRITE_ONCE(work->state, KSMBD_WORK_CANCELLED); } spin_lock(&fp->f_lock); - list_add_tail(&in_work->notify_entry, &fp->notify_pendings); + list_del_init(&work->fp_entry); spin_unlock(&fp->f_lock); - ksmbd_fd_put(work, fp); + rsp->hdr.Status = work->state == KSMBD_WORK_CLOSED ? + STATUS_NOTIFY_CLEANUP : STATUS_CANCELLED; + smb2_send_interim_resp(work, rsp->hdr.Status); + release_async_work(work); work->send_no_response = 1; + + ksmbd_fd_put(work, fp); return 0; } diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index c28e3d65d..f11f322fe 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -616,7 +616,6 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) { struct file *filp; struct ksmbd_lock *smb_lock, *tmp_lock; - struct ksmbd_work *cn_work; fd_limit_close(); ksmbd_remove_durable_fd(fp); @@ -649,52 +648,6 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) kfree(smb_lock); } - /* - * Complete any CHANGE_NOTIFY left pending on this handle now that - * it is closed. KSMBD never completes CHANGE_NOTIFY spontaneously - * (no real change-notification backend), only on close -- matching - * genuine SMB2/macOS smbfs semantics and avoiding the Finder - * "directory changed, re-enumerate everything" loop. - * - * smb2_notify() on another connection can be adding to - * notify_pendings under fp->f_lock at the same time this handle is - * closed, and a client-sent CANCEL can concurrently be racing to - * claim the same entry via smb2_notify_cancel_fn() (smb2pdu.c). - * Pop one entry at a time under the lock via list_del_init() rather - * than a bulk list_splice_init(): list_del_init() leaves the node - * self-linked ("empty"), which is what the cancel path checks under - * the same lock to tell whether it lost the race -- a bulk splice - * would instead relink every entry into a shared local list, so an - * entry claimed here would still read as "not empty" to a racing - * cancel_fn, and both sides could end up freeing the same work. - * ksmbd_conn_write() can sleep (it takes conn's write mutex), so it - * must not be called while fp->f_lock is held -- release the lock - * before processing each popped entry, then reacquire it for the - * next. - */ - for (;;) { - spin_lock(&fp->f_lock); - if (list_empty(&fp->notify_pendings)) { - spin_unlock(&fp->f_lock); - break; - } - cn_work = list_first_entry(&fp->notify_pendings, - struct ksmbd_work, notify_entry); - list_del_init(&cn_work->notify_entry); - spin_unlock(&fp->f_lock); - - ksmbd_conn_write(cn_work); - /* - * release_async_work() removes cn_work from - * conn->async_requests, frees cancel_argv, and releases+zeroes - * async_id -- all needed before ksmbd_free_work_struct(), which - * only releases async_id itself if still nonzero (i.e. if this - * hadn't already been done). - */ - release_async_work(cn_work); - ksmbd_free_work_struct(cn_work); - } - /* * Drop fp's strong reference on conn (taken in ksmbd_open_fd() / * ksmbd_reopen_durable_fd()). Durable fps that reached the @@ -1221,7 +1174,6 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) INIT_LIST_HEAD(&fp->blocked_works); INIT_LIST_HEAD(&fp->node); INIT_LIST_HEAD(&fp->lock_list); - INIT_LIST_HEAD(&fp->notify_pendings); spin_lock_init(&fp->f_lock); mutex_init(&fp->readdir_lock); atomic_set(&fp->refcount, 1); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 5fac4b0b4..11bfbb981 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -157,12 +157,6 @@ struct ksmbd_file { unsigned int outstanding_requests; unsigned int outstanding_pre_requests; struct ksmbd_lock_sequence lock_seq[KSMBD_LOCK_SEQ_ARRAY_SIZE]; - - /* - * Pending CHANGE_NOTIFY completions for this handle, sent with - * STATUS_NOTIFY_CLEANUP when the handle is closed. - */ - struct list_head notify_pendings; }; static inline void set_ctx_actor(struct dir_context *ctx, base-commit: e3ebfe6d5045a2365caae7f8cb988b1d087bbbef -- 2.43.0