Linux CIFS filesystem development
 help / color / mirror / Atom feed
* [PATCH 00/12] smb/server: change notify support
@ 2026-09-26  9:05 ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 01/12] smb/server: move change notify handling into notify.c ChenXiaoSong
                   ` (11 more replies)
  0 siblings, 12 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

rfc v1 -> v1:
  - Patch #02: STATUS_INVALID_PARAMETER for CHANGE_NOTIFY on non-directory opens.
  - Patch #04: Do not reject a CompletionFilter if no mapped bits remain, keep the request pending.
  - Patch #04: Do not merge later CHANGE_NOTIFY filters into the existing watch.
  - Patch #04: Add a mapping for `FILE_NOTIFY_CHANGE_SIZE` in `ksmbd_notify_mapping`.
  - Patch #11: Give queued events to the oldest pending waiter first.
  - Patch #11: Use the current request's `OutputBufferLength` instead of `notify->max_buffer_size`.

rfc v1: https://lore.kernel.org/linux-cifs/20260723031644.312866-1-chenxiaosong@chenxiaosong.com/

TODO: stream-related notify.

ChenXiaoSong (12):
  smb/server: move change notify handling into notify.c
  smb/server: add more validation for change notify requests
  smb/server: add debug type for change notify
  smb/server: support non-recursive directory change watches
  smb/server: support recursive directory change watches
  smb/server: keep notify watches on file handles
  smb/server: save simple notify events
  smb/server: save old names for rename notify events
  smb/server: match rename notify events
  smb/server: encode notify events
  smb/server: send notify events to the client
  smb/server: break directory leases before sending notify events

 fs/smb/server/Kconfig     |    1 +
 fs/smb/server/Makefile    |    3 +-
 fs/smb/server/glob.h      |    3 +-
 fs/smb/server/notify.c    | 1193 +++++++++++++++++++++++++++++++++++++
 fs/smb/server/notify.h    |   26 +
 fs/smb/server/oplock.c    |   40 ++
 fs/smb/server/oplock.h    |    1 +
 fs/smb/server/server.c    |    2 +-
 fs/smb/server/smb2pdu.c   |  112 +---
 fs/smb/server/vfs_cache.c |    3 +
 fs/smb/server/vfs_cache.h |    4 +
 11 files changed, 1278 insertions(+), 110 deletions(-)
 create mode 100644 fs/smb/server/notify.c
 create mode 100644 fs/smb/server/notify.h

-- 
2.55.0


^ permalink raw reply	[flat|nested] 17+ messages in thread

* [PATCH 01/12] smb/server: move change notify handling into notify.c
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 02/12] smb/server: add more validation for change notify requests ChenXiaoSong
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Move the SMB2 CHANGE_NOTIFY implementation out of smb2pdu.c into a
dedicated notify.c file.

Factor out two helper functions ksmbd_notify_validate_req() and
ksmbd_notify_wait().

No functional change.

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/Makefile  |   3 +-
 fs/smb/server/notify.c  | 183 ++++++++++++++++++++++++++++++++++++++++
 fs/smb/server/notify.h  |  24 ++++++
 fs/smb/server/smb2pdu.c | 110 +-----------------------
 4 files changed, 213 insertions(+), 107 deletions(-)
 create mode 100644 fs/smb/server/notify.c
 create mode 100644 fs/smb/server/notify.h

diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile
index 9bc87695a53c..5daae1727b32 100644
--- a/fs/smb/server/Makefile
+++ b/fs/smb/server/Makefile
@@ -10,7 +10,8 @@ ksmbd-y :=	unicode.o auth.o vfs.o vfs_cache.o server.o ndr.o \
 		mgmt/tree_connect.o mgmt/user_session.o smb_common.o \
 		transport_tcp.o transport_ipc.o smbacl.o smb2pdu.o \
 		smb2ops.o smb2misc.o ksmbd_spnego_negtokeninit.asn1.o \
-		ksmbd_spnego_negtokentarg.asn1.o asn1.o compress.o
+		ksmbd_spnego_negtokentarg.asn1.o asn1.o compress.o \
+		notify.o
 
 $(obj)/asn1.o: $(obj)/ksmbd_spnego_negtokeninit.asn1.h $(obj)/ksmbd_spnego_negtokentarg.asn1.h
 
diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
new file mode 100644
index 000000000000..7e324af36b47
--- /dev/null
+++ b/fs/smb/server/notify.c
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ *   SMB2 CHANGE_NOTIFY
+ *
+ *   Copyright (C) 2026 KylinSoft Co., Ltd. All rights reserved.
+ *
+ *   Author(s): ChenXiaoSong <chenxiaosong@kylinos.cn>
+ *              Gael Blivet <gael.blivet@gmail.com>
+ *
+ */
+
+#include "glob.h"
+#include "../common/smb2status.h"
+#include "connection.h"
+#include "ksmbd_work.h"
+#include "notify.h"
+#include "smb_common.h"
+#include "smb2pdu.h"
+#include "vfs_cache.h"
+#include "mgmt/user_session.h"
+
+struct ksmbd_notify_req {
+	wait_queue_head_t wait;
+};
+
+/*
+ * 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_notify_req *notify_req = argv[0];
+
+	wake_up(&notify_req->wait);
+}
+
+static struct ksmbd_file *
+ksmbd_notify_validate_req(struct ksmbd_work *work,
+			  struct smb2_change_notify_req *req,
+			  struct smb2_change_notify_rsp *rsp)
+{
+	struct ksmbd_file *fp;
+
+	if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) {
+		pr_err("Notify request is not the last compound command\n");
+		rsp->hdr.Status = STATUS_INTERNAL_ERROR;
+		return ERR_PTR(-EIO);
+	}
+
+	fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId,
+				  req->PersistentFileId);
+	if (!fp) {
+		pr_err("Invalid file id for notify, fid %llu:%llu\n",
+		       le64_to_cpu(req->PersistentFileId),
+		       le64_to_cpu(req->VolatileFileId));
+		rsp->hdr.Status = STATUS_FILE_CLOSED;
+		return ERR_PTR(-ENOENT);
+	}
+
+	return fp;
+}
+
+static int ksmbd_notify_wait(struct ksmbd_work *work,
+			     struct ksmbd_file *fp,
+			     struct ksmbd_notify_req *notify_req)
+{
+	int err;
+
+	/*
+	 * Handle close holds the file-table write lock while it marks the
+	 * handle closed and walks blocked_works.  Hold the matching read lock
+	 * across the state check and registration so close cannot finish its
+	 * walk between the lookup above and this list insertion.
+	 */
+	read_lock(&work->sess->file_table.lock);
+	if (fp->f_state != FP_INITED) {
+		read_unlock(&work->sess->file_table.lock);
+		return -ENOENT;
+	}
+	spin_lock(&fp->f_lock);
+	list_add_tail(&work->fp_entry, &fp->blocked_works);
+	spin_unlock(&fp->f_lock);
+	read_unlock(&work->sess->file_table.lock);
+
+	smb2_send_interim_resp(work, STATUS_PENDING);
+
+	err = wait_event_interruptible(notify_req->wait,
+				       READ_ONCE(work->state) !=
+					       KSMBD_WORK_ACTIVE);
+	if (err && READ_ONCE(work->state) == KSMBD_WORK_ACTIVE) {
+		pr_err("Notify wait interrupted, async id %d: %d\n",
+		       work->async_id, err);
+		/*
+		 * 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_del_init(&work->fp_entry);
+	spin_unlock(&fp->f_lock);
+
+	return err;
+}
+
+/**
+ * ksmbd_handle_notify() - handle an SMB2 change notify request
+ * @work: smb work containing notify command buffer
+ * @req: SMB2 change notify request
+ * @rsp: SMB2 change notify response
+ *
+ * Return: 0 on success, otherwise error
+ */
+int ksmbd_handle_notify(struct ksmbd_work *work,
+			struct smb2_change_notify_req *req,
+			struct smb2_change_notify_rsp *rsp)
+{
+	struct ksmbd_notify_req notify_req = {};
+	struct ksmbd_file *fp = NULL;
+	void **argv = NULL;
+	bool async_work = false;
+	int err = 0;
+
+	fp = ksmbd_notify_validate_req(work, req, rsp);
+	if (IS_ERR(fp)) {
+		err = PTR_ERR(fp);
+		fp = NULL;
+		goto out;
+	}
+
+	argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP);
+	if (!argv) {
+		pr_err("Failed to allocate notify cancel arguments\n");
+		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
+		err = -ENOMEM;
+		goto out;
+	}
+	init_waitqueue_head(&notify_req.wait);
+	argv[0] = &notify_req;
+
+	err = setup_async_work(work, smb2_notify_cancel, argv);
+	if (err) {
+		pr_err("Failed to set up asynchronous notify work: %d\n", err);
+		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
+		goto out;
+	}
+	async_work = true;
+
+	err = ksmbd_notify_wait(work, fp, &notify_req);
+	if (err == -ENOENT) {
+		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
+		goto out;
+	}
+
+	if (work->state == KSMBD_WORK_CLOSED) {
+		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
+	} else {
+		rsp->hdr.Status = STATUS_CANCELLED;
+	}
+	smb2_send_interim_resp(work, rsp->hdr.Status);
+	work->send_no_response = 1;
+
+out:
+	if (rsp->hdr.Status != STATUS_SUCCESS && !work->send_no_response)
+		smb2_set_err_rsp(work);
+	if (async_work)
+		release_async_work(work);
+	else
+		kfree(argv);
+	if (fp)
+		ksmbd_fd_put(work, fp);
+	return err;
+}
diff --git a/fs/smb/server/notify.h b/fs/smb/server/notify.h
new file mode 100644
index 000000000000..8de46e07b02e
--- /dev/null
+++ b/fs/smb/server/notify.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ *   SMB2 CHANGE_NOTIFY
+ *
+ *   Copyright (C) 2026 KylinSoft Co., Ltd. All rights reserved.
+ *
+ *   Author(s): ChenXiaoSong <chenxiaosong@kylinos.cn>
+ *              Gael Blivet <gael.blivet@gmail.com>
+ *
+ */
+
+#ifndef __SMB_SERVER_NOTIFY_H__
+#define __SMB_SERVER_NOTIFY_H__
+
+struct ksmbd_work;
+struct smb2_change_notify_req;
+struct smb2_change_notify_rsp;
+
+int ksmbd_handle_notify(struct ksmbd_work *work,
+			struct smb2_change_notify_req *req,
+			struct smb2_change_notify_rsp *rsp);
+
+#endif /* __SMB_SERVER_NOTIFY_H__ */
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 7b9080508a3f..1f45a0836fe9 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -33,6 +33,7 @@
 #include "vfs.h"
 #include "vfs_cache.h"
 #include "misc.h"
+#include "notify.h"
 
 #include "server.h"
 #include "smb_common.h"
@@ -11818,28 +11819,6 @@ int smb2_oplock_break(struct ksmbd_work *work)
 	return 0;
 }
 
-struct ksmbd_notify_req {
-	wait_queue_head_t wait;
-};
-
-/*
- * 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_notify_req *notify_req = argv[0];
-
-	wake_up(&notify_req->wait);
-}
-
 /**
  * smb2_notify() - handler for smb2 notify request
  * @work:   smb work containing notify command buffer
@@ -11850,98 +11829,17 @@ int smb2_notify(struct ksmbd_work *work)
 {
 	struct smb2_change_notify_req *req;
 	struct smb2_change_notify_rsp *rsp;
-	struct ksmbd_notify_req notify_req;
-	struct ksmbd_file *fp = NULL;
-	void **argv = NULL;
-	bool async_work = false;
-	int err = 0;
 
 	ksmbd_debug(SMB, "Received smb2 notify\n");
 
 	WORK_BUFFERS(work, req, rsp);
 
-	if (smb2_compound_has_failed(work, &rsp->hdr))
+	if (smb2_compound_has_failed(work, &rsp->hdr)) {
+		pr_err("Failed compound notify request\n");
 		return -EACCES;
-
-	if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) {
-		rsp->hdr.Status = STATUS_INTERNAL_ERROR;
-		err = -EIO;
-		goto out;
-	}
-
-	fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId);
-	if (!fp) {
-		rsp->hdr.Status = STATUS_FILE_CLOSED;
-		err = -ENOENT;
-		goto out;
-	}
-
-	argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP);
-	if (!argv) {
-		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
-		err = -ENOMEM;
-		goto out;
-	}
-	init_waitqueue_head(&notify_req.wait);
-	argv[0] = &notify_req;
-
-	err = setup_async_work(work, smb2_notify_cancel, argv);
-	if (err) {
-		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
-		goto out;
-	}
-	async_work = true;
-
-	/*
-	 * Handle close holds the file-table write lock while it marks the
-	 * handle closed and walks blocked_works.  Hold the matching read lock
-	 * across the state check and registration so close cannot finish its
-	 * walk between the lookup above and this list insertion.
-	 */
-	read_lock(&work->sess->file_table.lock);
-	if (fp->f_state != FP_INITED) {
-		read_unlock(&work->sess->file_table.lock);
-		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
-		err = -ENOENT;
-		goto out;
 	}
-	spin_lock(&fp->f_lock);
-	list_add_tail(&work->fp_entry, &fp->blocked_works);
-	spin_unlock(&fp->f_lock);
-	read_unlock(&work->sess->file_table.lock);
 
-	smb2_send_interim_resp(work, STATUS_PENDING);
-
-	err = wait_event_interruptible(notify_req.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_del_init(&work->fp_entry);
-	spin_unlock(&fp->f_lock);
-
-	rsp->hdr.Status = work->state == KSMBD_WORK_CLOSED ?
-			  STATUS_NOTIFY_CLEANUP : STATUS_CANCELLED;
-	smb2_send_interim_resp(work, rsp->hdr.Status);
-	work->send_no_response = 1;
-
-out:
-	if (rsp->hdr.Status != STATUS_SUCCESS && !work->send_no_response)
-		smb2_set_err_rsp(work);
-	if (async_work)
-		release_async_work(work);
-	else
-		kfree(argv);
-	if (fp)
-		ksmbd_fd_put(work, fp);
-	return err;
+	return ksmbd_handle_notify(work, req, rsp);
 }
 
 /**
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 02/12] smb/server: add more validation for change notify requests
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 01/12] smb/server: move change notify handling into notify.c ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 03/12] smb/server: add debug type for change notify ChenXiaoSong
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Add validation for the file type, directory list access and output
buffer length before setting up a change notify watch.

Suggested-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index 7e324af36b47..18cf0005a037 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -48,6 +48,7 @@ ksmbd_notify_validate_req(struct ksmbd_work *work,
 			  struct smb2_change_notify_rsp *rsp)
 {
 	struct ksmbd_file *fp;
+	int err;
 
 	if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) {
 		pr_err("Notify request is not the last compound command\n");
@@ -65,7 +66,37 @@ ksmbd_notify_validate_req(struct ksmbd_work *work,
 		return ERR_PTR(-ENOENT);
 	}
 
+	if (le32_to_cpu(req->OutputBufferLength) >
+	    work->conn->vals->max_trans_size) {
+		pr_err("Notify output buffer length %u exceeds maximum %u\n",
+		       le32_to_cpu(req->OutputBufferLength),
+		       work->conn->vals->max_trans_size);
+		rsp->hdr.Status = STATUS_INVALID_PARAMETER;
+		err = -EINVAL;
+		goto err_put_fp;
+	}
+
+	if (!S_ISDIR(file_inode(fp->filp)->i_mode)) {
+		pr_err("Notify file id is not a directory, fid %llu:%llu\n",
+		       fp->persistent_id, fp->volatile_id);
+		rsp->hdr.Status = STATUS_INVALID_PARAMETER;
+		err = -EINVAL;
+		goto err_put_fp;
+	}
+
+	if (!(fp->daccess & FILE_LIST_DIRECTORY_LE)) {
+		pr_err("No permission to monitor directory, fid %llu:%llu\n",
+		       fp->persistent_id, fp->volatile_id);
+		rsp->hdr.Status = STATUS_ACCESS_DENIED;
+		err = -EACCES;
+		goto err_put_fp;
+	}
+
 	return fp;
+
+err_put_fp:
+	ksmbd_fd_put(work, fp);
+	return ERR_PTR(err);
 }
 
 static int ksmbd_notify_wait(struct ksmbd_work *work,
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 03/12] smb/server: add debug type for change notify
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 01/12] smb/server: move change notify handling into notify.c ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 02/12] smb/server: add more validation for change notify requests ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 04/12] smb/server: support non-recursive directory change watches ChenXiaoSong
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Add KSMBD_DEBUG_NOTIFY and expose it as "notify" to print logs
for the change notify feature.

Example:

  ksmbd.control --debug=notify
  [ksmbd.control/802]: INFO: smb auth vfs oplock ipc conn rdma [notify]

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/glob.h    |  3 ++-
 fs/smb/server/notify.c  | 12 ++++++++++++
 fs/smb/server/server.c  |  2 +-
 fs/smb/server/smb2pdu.c |  2 +-
 4 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/fs/smb/server/glob.h b/fs/smb/server/glob.h
index 4ea187af2348..5131bad88d96 100644
--- a/fs/smb/server/glob.h
+++ b/fs/smb/server/glob.h
@@ -21,10 +21,11 @@ extern int ksmbd_debug_types;
 #define KSMBD_DEBUG_IPC         BIT(4)
 #define KSMBD_DEBUG_CONN        BIT(5)
 #define KSMBD_DEBUG_RDMA        BIT(6)
+#define KSMBD_DEBUG_NOTIFY      BIT(7)
 #define KSMBD_DEBUG_ALL         (KSMBD_DEBUG_SMB | KSMBD_DEBUG_AUTH |	\
 				KSMBD_DEBUG_VFS | KSMBD_DEBUG_OPLOCK |	\
 				KSMBD_DEBUG_IPC | KSMBD_DEBUG_CONN |	\
-				KSMBD_DEBUG_RDMA)
+				KSMBD_DEBUG_RDMA | KSMBD_DEBUG_NOTIFY)
 
 #ifdef pr_fmt
 #undef pr_fmt
diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index 18cf0005a037..502ec551c01e 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -39,6 +39,7 @@ static void smb2_notify_cancel(void **argv)
 {
 	struct ksmbd_notify_req *notify_req = argv[0];
 
+	ksmbd_debug(NOTIFY, "Wake pending notify request\n");
 	wake_up(&notify_req->wait);
 }
 
@@ -66,6 +67,11 @@ ksmbd_notify_validate_req(struct ksmbd_work *work,
 		return ERR_PTR(-ENOENT);
 	}
 
+	ksmbd_debug(NOTIFY,
+		    "fid %llu:%llu, handle notify request, filter 0x%x, flags 0x%x\n",
+		    fp->persistent_id, fp->volatile_id,
+		    le32_to_cpu(req->CompletionFilter), le16_to_cpu(req->Flags));
+
 	if (le32_to_cpu(req->OutputBufferLength) >
 	    work->conn->vals->max_trans_size) {
 		pr_err("Notify output buffer length %u exceeds maximum %u\n",
@@ -121,6 +127,8 @@ static int ksmbd_notify_wait(struct ksmbd_work *work,
 	spin_unlock(&fp->f_lock);
 	read_unlock(&work->sess->file_table.lock);
 
+	ksmbd_debug(NOTIFY, "Notify request pending, async id %d\n",
+		    work->async_id);
 	smb2_send_interim_resp(work, STATUS_PENDING);
 
 	err = wait_event_interruptible(notify_req->wait,
@@ -195,8 +203,12 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 
 	if (work->state == KSMBD_WORK_CLOSED) {
 		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
+		ksmbd_debug(NOTIFY, "Notify handle closed, async id %d\n",
+			    work->async_id);
 	} else {
 		rsp->hdr.Status = STATUS_CANCELLED;
+		ksmbd_debug(NOTIFY, "Notify request cancelled, async id %d\n",
+			    work->async_id);
 	}
 	smb2_send_interim_resp(work, rsp->hdr.Status);
 	work->send_no_response = 1;
diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c
index 0827c8c51006..958e952223aa 100644
--- a/fs/smb/server/server.c
+++ b/fs/smb/server/server.c
@@ -564,7 +564,7 @@ static ssize_t kill_server_store(const struct class *class,
 
 static const char * const debug_type_strings[] = {"smb", "auth", "vfs",
 						  "oplock", "ipc", "conn",
-						  "rdma"};
+						  "rdma", "notify"};
 
 static ssize_t debug_show(const struct class *class, const struct class_attribute *attr,
 			  char *buf)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 1f45a0836fe9..eb15cda699b7 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -11830,7 +11830,7 @@ int smb2_notify(struct ksmbd_work *work)
 	struct smb2_change_notify_req *req;
 	struct smb2_change_notify_rsp *rsp;
 
-	ksmbd_debug(SMB, "Received smb2 notify\n");
+	ksmbd_debug(NOTIFY, "Received smb2 notify\n");
 
 	WORK_BUFFERS(work, req, rsp);
 
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 04/12] smb/server: support non-recursive directory change watches
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (2 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 03/12] smb/server: add debug type for change notify ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-27 10:51   ` Namjae Jeon
  2026-09-26  9:05 ` [PATCH 05/12] smb/server: support recursive " ChenXiaoSong
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Use fsnotify to watch a directory for changes. Add one watch for each
SMB2 CHANGE_NOTIFY request with SMB2_WATCH_TREE unset.

Example:

  1. client: smbinfo notify /mnt
  2. server: ksmbd.control --debug=notify
  3. server: touch /export/file
  4. server debug log:
     ksmbd: fid 4:4, notify event: mask=0x00000100 inode=2 name=file cookie=0
     ksmbd: fid 4:4, notify event: mask=0x08000004 inode=2 name=file cookie=0

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/Kconfig  |   1 +
 fs/smb/server/notify.c | 232 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 231 insertions(+), 2 deletions(-)

diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig
index b7665e0e4942..1f91926151b2 100644
--- a/fs/smb/server/Kconfig
+++ b/fs/smb/server/Kconfig
@@ -19,6 +19,7 @@ config SMB_SERVER
 	select ASN1
 	select OID_REGISTRY
 	select CRC32
+	select FSNOTIFY
 	default n
 	help
 	  Choose Y here if you want to allow SMB3 compliant clients
diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index 502ec551c01e..b85f5fdb5066 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -10,6 +10,7 @@
  *
  */
 
+#include <linux/fsnotify_backend.h>
 #include "glob.h"
 #include "../common/smb2status.h"
 #include "connection.h"
@@ -20,10 +21,57 @@
 #include "vfs_cache.h"
 #include "mgmt/user_session.h"
 
+struct ksmbd_notify {
+	struct fsnotify_group *group;
+	struct fsnotify_mark *mark;
+	struct ksmbd_file *fp;
+	/* Protects filter, rename state and the queued events. */
+	spinlock_t lock;
+	u32 filter;
+	u32 mask;
+};
+
 struct ksmbd_notify_req {
 	wait_queue_head_t wait;
 };
 
+#define KSMBD_NOTIFY_NAME_EVENT_MASK	(FS_CREATE | FS_DELETE | \
+					 FS_MOVED_FROM | FS_MOVED_TO)
+
+static const struct {
+	u32 notify_mask;
+	u32 fsnotify_mask;
+} ksmbd_notify_mapping[] = {
+	{ FILE_NOTIFY_CHANGE_FILE_NAME,
+	  KSMBD_NOTIFY_NAME_EVENT_MASK },
+	{ FILE_NOTIFY_CHANGE_DIR_NAME,
+	  KSMBD_NOTIFY_NAME_EVENT_MASK },
+	{ FILE_NOTIFY_CHANGE_ATTRIBUTES,
+	  FS_ATTRIB | FS_MOVED_FROM | FS_MOVED_TO | FS_MODIFY },
+	{ FILE_NOTIFY_CHANGE_SIZE, FS_MODIFY },
+	{ FILE_NOTIFY_CHANGE_LAST_WRITE, FS_ATTRIB },
+	{ FILE_NOTIFY_CHANGE_LAST_ACCESS, FS_ATTRIB },
+	{ FILE_NOTIFY_CHANGE_EA, FS_ATTRIB },
+	{ FILE_NOTIFY_CHANGE_SECURITY, FS_ATTRIB },
+};
+
+static u32 ksmbd_notify_map(u32 filter)
+{
+	size_t i;
+	u32 mask = 0;
+
+	for (i = 0; i < ARRAY_SIZE(ksmbd_notify_mapping); i++) {
+		if (ksmbd_notify_mapping[i].notify_mask & filter)
+			mask |= ksmbd_notify_mapping[i].fsnotify_mask;
+	}
+
+	ksmbd_debug(NOTIFY,
+		    "Mapped completion filter 0x%x to fsnotify mask 0x%x\n",
+		    filter, mask);
+
+	return mask;
+}
+
 /*
  * Cancel handler for a pending CHANGE_NOTIFY. Called either by
  * smb2_cancel() (conn->request_lock held, work->state already set to
@@ -43,6 +91,140 @@ static void smb2_notify_cancel(void **argv)
 	wake_up(&notify_req->wait);
 }
 
+static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
+					   u32 mask, struct inode *inode,
+					   struct inode *dir,
+					   const struct qstr *file_name,
+					   u32 cookie)
+{
+	struct inode *event_inode = dir ?: inode;
+	struct ksmbd_file *fp;
+
+	fp = notify->fp;
+
+	ksmbd_debug(NOTIFY,
+		    "fid %llu:%llu, notify event: mask=0x%08x inode=%llu name=%.*s cookie=%u\n",
+		    fp->persistent_id, fp->volatile_id, mask,
+		    event_inode ? (unsigned long long)event_inode->i_ino : 0,
+		    file_name ? file_name->len : 0,
+		    file_name ? (const char *)file_name->name : "", cookie);
+
+	return 0;
+}
+
+static int ksmbd_notify_handle_event(struct fsnotify_group *group, u32 mask,
+				     const void *data, int data_type,
+				     struct inode *dir,
+				     const struct qstr *file_name, u32 cookie,
+				     struct fsnotify_iter_info *iter_info)
+{
+	struct ksmbd_notify *notify = group->private;
+	struct inode *inode = fsnotify_data_inode(data, data_type);
+
+	return ksmbd_notify_handle_inode_event(notify, mask, inode, dir,
+					      file_name, cookie);
+}
+
+static void ksmbd_notify_free_mark(struct fsnotify_mark *mark)
+{
+	kfree(mark);
+}
+
+static const struct fsnotify_ops ksmbd_notify_fsnotify_ops = {
+	.handle_event = ksmbd_notify_handle_event,
+	.free_mark = ksmbd_notify_free_mark,
+};
+
+static struct fsnotify_mark *
+ksmbd_notify_add_mark(struct ksmbd_notify *notify, u32 mask, void *obj,
+		      unsigned int obj_type)
+{
+	struct fsnotify_mark *mark;
+	int err;
+
+	mark = kzalloc_obj(*mark, KSMBD_DEFAULT_GFP);
+	if (!mark) {
+		pr_err("Failed to allocate fsnotify mark\n");
+		return ERR_PTR(-ENOMEM);
+	}
+
+	fsnotify_init_mark(mark, notify->group);
+	mark->mask = mask | FS_EVENT_ON_CHILD;
+	err = fsnotify_add_mark(mark, obj, obj_type, 0);
+	if (err) {
+		pr_err("Failed to add fsnotify mark, type %u: %d\n",
+		       obj_type, err);
+		goto err_put_mark;
+	}
+
+	return mark;
+
+err_put_mark:
+	fsnotify_put_mark(mark);
+	return ERR_PTR(err);
+}
+
+static void ksmbd_notify_destroy_marks(struct ksmbd_notify *notify)
+{
+	if (notify->mark) {
+		fsnotify_destroy_mark(notify->mark, notify->group);
+		fsnotify_put_mark(notify->mark);
+	}
+	fsnotify_wait_marks_destroyed();
+	fsnotify_put_group(notify->group);
+}
+
+static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
+			    struct ksmbd_notify **notify_out)
+{
+	struct ksmbd_notify *notify;
+	struct fsnotify_mark *mark;
+	int err = 0;
+
+	notify = kzalloc_obj(*notify, KSMBD_DEFAULT_GFP);
+	if (!notify) {
+		pr_err("Failed to allocate notify watch\n");
+		err = -ENOMEM;
+		goto out;
+	}
+
+	notify->fp = fp;
+	spin_lock_init(&notify->lock);
+	notify->filter = filter;
+	notify->mask = mask;
+
+	notify->group = fsnotify_alloc_group(&ksmbd_notify_fsnotify_ops, 0);
+	if (IS_ERR(notify->group)) {
+		err = PTR_ERR(notify->group);
+		pr_err("Failed to allocate fsnotify group: %d\n", err);
+		kfree(notify);
+		goto out;
+	}
+	notify->group->private = notify;
+
+	mark = ksmbd_notify_add_mark(notify, mask, file_inode(fp->filp),
+				     FSNOTIFY_OBJ_TYPE_INODE);
+	if (IS_ERR(mark)) {
+		err = PTR_ERR(mark);
+		goto err_destroy_marks;
+	}
+	notify->mark = mark;
+
+	*notify_out = notify;
+	ksmbd_debug(NOTIFY,
+		    "Added fsnotify mark, inode %llu, mask 0x%x, filter 0x%x\n",
+		    (unsigned long long)file_inode(fp->filp)->i_ino, mark->mask,
+		    filter);
+	goto out;
+
+err_destroy_marks:
+	ksmbd_notify_destroy_marks(notify);
+	kfree(notify);
+
+out:
+	return err;
+}
+
 static struct ksmbd_file *
 ksmbd_notify_validate_req(struct ksmbd_work *work,
 			  struct smb2_change_notify_req *req,
@@ -82,6 +264,13 @@ ksmbd_notify_validate_req(struct ksmbd_work *work,
 		goto err_put_fp;
 	}
 
+	if (le16_to_cpu(req->Flags) & ~SMB2_WATCH_TREE) {
+		pr_err("Invalid notify flags 0x%x\n", le16_to_cpu(req->Flags));
+		rsp->hdr.Status = STATUS_INVALID_PARAMETER;
+		err = -EINVAL;
+		goto err_put_fp;
+	}
+
 	if (!S_ISDIR(file_inode(fp->filp)->i_mode)) {
 		pr_err("Notify file id is not a directory, fid %llu:%llu\n",
 		       fp->persistent_id, fp->volatile_id);
@@ -105,10 +294,37 @@ ksmbd_notify_validate_req(struct ksmbd_work *work,
 	return ERR_PTR(err);
 }
 
+static struct ksmbd_notify *
+ksmbd_notify_setup_watch(struct ksmbd_file *fp,
+			 struct smb2_change_notify_req *req,
+			 struct smb2_change_notify_rsp *rsp)
+{
+	struct ksmbd_notify *notify;
+	u32 filter, mask;
+	int err;
+
+	filter = le32_to_cpu(req->CompletionFilter);
+	mask = ksmbd_notify_map(filter);
+	if (!mask)
+		ksmbd_debug(NOTIFY,
+			    "No mapped completion filter bits; request will remain pending\n");
+
+	err = ksmbd_notify_add(fp, mask, filter, &notify);
+	if (err) {
+		pr_err("Failed to add notify watch, fid %llu:%llu: %d\n",
+		       fp->persistent_id, fp->volatile_id, err);
+		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
+		return ERR_PTR(err);
+	}
+
+	return notify;
+}
+
 static int ksmbd_notify_wait(struct ksmbd_work *work,
-			     struct ksmbd_file *fp,
+			     struct ksmbd_notify *notify,
 			     struct ksmbd_notify_req *notify_req)
 {
+	struct ksmbd_file *fp = notify->fp;
 	int err;
 
 	/*
@@ -165,6 +381,7 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 			struct smb2_change_notify_rsp *rsp)
 {
 	struct ksmbd_notify_req notify_req = {};
+	struct ksmbd_notify *notify = NULL;
 	struct ksmbd_file *fp = NULL;
 	void **argv = NULL;
 	bool async_work = false;
@@ -177,6 +394,13 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 		goto out;
 	}
 
+	notify = ksmbd_notify_setup_watch(fp, req, rsp);
+	if (IS_ERR(notify)) {
+		err = PTR_ERR(notify);
+		notify = NULL;
+		goto out;
+	}
+
 	argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP);
 	if (!argv) {
 		pr_err("Failed to allocate notify cancel arguments\n");
@@ -195,7 +419,7 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 	}
 	async_work = true;
 
-	err = ksmbd_notify_wait(work, fp, &notify_req);
+	err = ksmbd_notify_wait(work, notify, &notify_req);
 	if (err == -ENOENT) {
 		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
 		goto out;
@@ -220,6 +444,10 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 		release_async_work(work);
 	else
 		kfree(argv);
+	if (notify) {
+		ksmbd_notify_destroy_marks(notify);
+		kfree(notify);
+	}
 	if (fp)
 		ksmbd_fd_put(work, fp);
 	return err;
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 05/12] smb/server: support recursive directory change watches
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (3 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 04/12] smb/server: support non-recursive directory change watches ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 06/12] smb/server: keep notify watches on file handles ChenXiaoSong
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Support SMB2 CHANGE_NOTIFY requests with SMB2_WATCH_TREE set.

Example:

  1. client: smbinfo notify /mnt
  2. server: ksmbd.control --debug=notify
  3. server: mkdir /export/dir; touch /export/dir/file
  4. server debug log:
     ksmbd: fid 1:1, notify event: mask=0x00000100 inode=11 name=dir\file cookie=0
     ksmbd: fid 1:1, notify event: mask=0x00000004 inode=11 name=dir\file cookie=0

Suggested-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 155 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 149 insertions(+), 6 deletions(-)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index b85f5fdb5066..b34f2f9b6d3f 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -11,6 +11,7 @@
  */
 
 #include <linux/fsnotify_backend.h>
+#include <linux/dcache.h>
 #include "glob.h"
 #include "../common/smb2status.h"
 #include "connection.h"
@@ -24,11 +25,13 @@
 struct ksmbd_notify {
 	struct fsnotify_group *group;
 	struct fsnotify_mark *mark;
+	struct fsnotify_mark *tree_mark;
 	struct ksmbd_file *fp;
 	/* Protects filter, rename state and the queued events. */
 	spinlock_t lock;
 	u32 filter;
 	u32 mask;
+	bool watch_tree;
 };
 
 struct ksmbd_notify_req {
@@ -112,6 +115,100 @@ static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 	return 0;
 }
 
+static char *ksmbd_notify_tree_name(struct ksmbd_notify *notify,
+				    const void *data, int data_type,
+				    struct inode *dir,
+				    const struct qstr *file_name,
+				    struct qstr *tree_name)
+{
+	struct dentry *root = file_dentry(notify->fp->filp);
+	struct inode *inode = fsnotify_data_inode(data, data_type);
+	struct dentry *dentry;
+	char *buf = NULL, *root_buf = NULL, *name = NULL;
+	char *path, *root_path, *relative;
+	size_t file_name_len = file_name ? file_name->len : 0;
+	size_t prefix_len, root_len, len, i;
+
+	/* Name events refer to @file_name below @dir. */
+	if (file_name && dir) {
+		dentry = d_find_alias(dir);
+	} else {
+		dentry = fsnotify_data_dentry(data, data_type);
+		if (dentry)
+			dget(dentry);
+		else
+			dentry = inode ? d_find_alias(inode) : NULL;
+	}
+	if (!dentry)
+		return ERR_PTR(-ENOENT);
+
+	/* A superblock mark also reports events outside the watched tree. */
+	if (!is_subdir(dentry, root)) {
+		name = ERR_PTR(-EXDEV);
+		goto out_dput;
+	}
+
+	buf = kmalloc(PATH_MAX, KSMBD_DEFAULT_GFP);
+	root_buf = kmalloc(PATH_MAX, KSMBD_DEFAULT_GFP);
+	if (!buf || !root_buf) {
+		name = ERR_PTR(-ENOMEM);
+		goto out_free_bufs;
+	}
+
+	path = dentry_path_raw(dentry, buf, PATH_MAX);
+	root_path = dentry_path_raw(root, root_buf, PATH_MAX);
+	if (IS_ERR(path) || IS_ERR(root_path)) {
+		name = ERR_PTR(IS_ERR(path) ? PTR_ERR(path) : PTR_ERR(root_path));
+		goto out_free_bufs;
+	}
+
+	root_len = strlen(root_path);
+	if (root_len == 1 && root_path[0] == '/') {
+		relative = path + 1;
+	} else if (!strncmp(path, root_path, root_len) &&
+		   (path[root_len] == '/' || path[root_len] == '\0')) {
+		relative = path + root_len;
+		if (*relative == '/')
+			relative++;
+	} else {
+		/* The watched directory was renamed between the two snapshots. */
+		name = ERR_PTR(-EAGAIN);
+		goto out_free_bufs;
+	}
+
+	prefix_len = strlen(relative);
+	if (prefix_len > SIZE_MAX - file_name_len - 2) {
+		name = ERR_PTR(-EOVERFLOW);
+		goto out_free_bufs;
+	}
+	len = prefix_len + file_name_len + (prefix_len && file_name ? 1 : 0);
+	name = kmalloc(len + 1, KSMBD_DEFAULT_GFP);
+	if (!name) {
+		name = ERR_PTR(-ENOMEM);
+		goto out_free_bufs;
+	}
+
+	memcpy(name, relative, prefix_len);
+	if (prefix_len && file_name)
+		name[prefix_len++] = '\\';
+	if (file_name_len)
+		memcpy(name + prefix_len, file_name->name, file_name_len);
+	name[len] = '\0';
+	for (i = 0; i < len; i++) {
+		if (name[i] == '/')
+			name[i] = '\\';
+	}
+	tree_name->name = name;
+	tree_name->len = len;
+
+out_free_bufs:
+	kfree(root_buf);
+	kfree(buf);
+out_dput:
+	dput(dentry);
+	return name;
+}
+
 static int ksmbd_notify_handle_event(struct fsnotify_group *group, u32 mask,
 				     const void *data, int data_type,
 				     struct inode *dir,
@@ -119,10 +216,32 @@ static int ksmbd_notify_handle_event(struct fsnotify_group *group, u32 mask,
 				     struct fsnotify_iter_info *iter_info)
 {
 	struct ksmbd_notify *notify = group->private;
-	struct inode *inode = fsnotify_data_inode(data, data_type);
+	struct qstr tree_name = {};
+	struct inode *inode;
+	char *name = NULL;
+	int err;
+
+	if (!READ_ONCE(notify->watch_tree)) {
+		/* Non-tree watches accept events from the root inode mark only. */
+		if (!fsnotify_iter_inode_mark(iter_info) &&
+		    !fsnotify_iter_parent_mark(iter_info))
+			return 0;
+	} else {
+		name = ksmbd_notify_tree_name(notify, data, data_type, dir,
+					      file_name, &tree_name);
+		if (IS_ERR(name)) {
+			if (PTR_ERR(name) == -ENOMEM)
+				pr_err("Failed to allocate tree notify event name\n");
+			return 0;
+		}
+		file_name = &tree_name;
+	}
 
-	return ksmbd_notify_handle_inode_event(notify, mask, inode, dir,
+	inode = fsnotify_data_inode(data, data_type);
+	err = ksmbd_notify_handle_inode_event(notify, mask, inode, dir,
 					      file_name, cookie);
+	kfree(name);
+	return err;
 }
 
 static void ksmbd_notify_free_mark(struct fsnotify_mark *mark)
@@ -166,6 +285,10 @@ ksmbd_notify_add_mark(struct ksmbd_notify *notify, u32 mask, void *obj,
 
 static void ksmbd_notify_destroy_marks(struct ksmbd_notify *notify)
 {
+	if (notify->tree_mark) {
+		fsnotify_destroy_mark(notify->tree_mark, notify->group);
+		fsnotify_put_mark(notify->tree_mark);
+	}
 	if (notify->mark) {
 		fsnotify_destroy_mark(notify->mark, notify->group);
 		fsnotify_put_mark(notify->mark);
@@ -175,10 +298,11 @@ static void ksmbd_notify_destroy_marks(struct ksmbd_notify *notify)
 }
 
 static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
+			    bool watch_tree,
 			    struct ksmbd_notify **notify_out)
 {
 	struct ksmbd_notify *notify;
-	struct fsnotify_mark *mark;
+	struct fsnotify_mark *mark, *tree_mark;
 	int err = 0;
 
 	notify = kzalloc_obj(*notify, KSMBD_DEFAULT_GFP);
@@ -192,6 +316,7 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	spin_lock_init(&notify->lock);
 	notify->filter = filter;
 	notify->mask = mask;
+	notify->watch_tree = watch_tree;
 
 	notify->group = fsnotify_alloc_group(&ksmbd_notify_fsnotify_ops, 0);
 	if (IS_ERR(notify->group)) {
@@ -210,11 +335,27 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	}
 	notify->mark = mark;
 
+	if (watch_tree) {
+		/*
+		 * FS_EVENT_ON_CHILD covers only one level. A filesystem mark
+		 * supplies recursive events; the callback limits them to the
+		 * directory rooted at fp.
+		 */
+		tree_mark = ksmbd_notify_add_mark(notify, mask,
+						  file_inode(fp->filp)->i_sb,
+						  FSNOTIFY_OBJ_TYPE_SB);
+		if (IS_ERR(tree_mark)) {
+			err = PTR_ERR(tree_mark);
+			goto err_destroy_marks;
+		}
+		notify->tree_mark = tree_mark;
+	}
+
 	*notify_out = notify;
 	ksmbd_debug(NOTIFY,
-		    "Added fsnotify mark, inode %llu, mask 0x%x, filter 0x%x\n",
+		    "Added fsnotify mark, inode %llu, mask 0x%x, filter 0x%x, watch tree %d\n",
 		    (unsigned long long)file_inode(fp->filp)->i_ino, mark->mask,
-		    filter);
+		    filter, watch_tree);
 	goto out;
 
 err_destroy_marks:
@@ -309,7 +450,9 @@ ksmbd_notify_setup_watch(struct ksmbd_file *fp,
 		ksmbd_debug(NOTIFY,
 			    "No mapped completion filter bits; request will remain pending\n");
 
-	err = ksmbd_notify_add(fp, mask, filter, &notify);
+	err = ksmbd_notify_add(fp, mask, filter,
+			       le16_to_cpu(req->Flags) & SMB2_WATCH_TREE,
+			       &notify);
 	if (err) {
 		pr_err("Failed to add notify watch, fid %llu:%llu: %d\n",
 		       fp->persistent_id, fp->volatile_id, err);
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 06/12] smb/server: keep notify watches on file handles
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (4 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 05/12] smb/server: support recursive " ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 07/12] smb/server: save simple notify events ChenXiaoSong
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Keep one notify watch on each file handle. Reuse it for later requests,
and remove it when the handle is closed.

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c    | 44 +++++++++++++++++++++++++++++++++++----
 fs/smb/server/notify.h    |  2 ++
 fs/smb/server/vfs_cache.c |  3 +++
 fs/smb/server/vfs_cache.h |  4 ++++
 4 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index b34f2f9b6d3f..b49c0ec5117d 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -305,6 +305,18 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	struct fsnotify_mark *mark, *tree_mark;
 	int err = 0;
 
+	mutex_lock(&fp->notify_lock);
+	if (fp->notify) {
+		/* Further requests on this open use the first request's options. */
+		ksmbd_debug(NOTIFY,
+			    "Reusing fsnotify mark, inode %llu, mask 0x%x, filter 0x%x, watch tree %d\n",
+			    (unsigned long long)file_inode(fp->filp)->i_ino,
+			    fp->notify->mark->mask, fp->notify->filter,
+			    fp->notify->watch_tree);
+		*notify_out = fp->notify;
+		goto out;
+	}
+
 	notify = kzalloc_obj(*notify, KSMBD_DEFAULT_GFP);
 	if (!notify) {
 		pr_err("Failed to allocate notify watch\n");
@@ -351,6 +363,7 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 		notify->tree_mark = tree_mark;
 	}
 
+	fp->notify = notify;
 	*notify_out = notify;
 	ksmbd_debug(NOTIFY,
 		    "Added fsnotify mark, inode %llu, mask 0x%x, filter 0x%x, watch tree %d\n",
@@ -363,9 +376,36 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	kfree(notify);
 
 out:
+	mutex_unlock(&fp->notify_lock);
 	return err;
 }
 
+/**
+ * ksmbd_notify_remove() - remove the notify watch for a closing handle
+ * @fp: file handle whose watch is being removed
+ *
+ * A cancelled CHANGE_NOTIFY request leaves this watch installed. The watch is
+ * owned by @fp and removed only when the file handle is finally closed.
+ */
+void ksmbd_notify_remove(struct ksmbd_file *fp)
+{
+	struct ksmbd_notify *notify;
+
+	mutex_lock(&fp->notify_lock);
+	notify = fp->notify;
+	fp->notify = NULL;
+	mutex_unlock(&fp->notify_lock);
+	if (!notify)
+		return;
+
+	ksmbd_debug(NOTIFY,
+		    "Removing fsnotify mark, inode %llu, mask 0x%x, watch tree %d\n",
+		    (unsigned long long)file_inode(fp->filp)->i_ino,
+		    notify->mark->mask, notify->watch_tree);
+	ksmbd_notify_destroy_marks(notify);
+	kfree(notify);
+}
+
 static struct ksmbd_file *
 ksmbd_notify_validate_req(struct ksmbd_work *work,
 			  struct smb2_change_notify_req *req,
@@ -587,10 +627,6 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 		release_async_work(work);
 	else
 		kfree(argv);
-	if (notify) {
-		ksmbd_notify_destroy_marks(notify);
-		kfree(notify);
-	}
 	if (fp)
 		ksmbd_fd_put(work, fp);
 	return err;
diff --git a/fs/smb/server/notify.h b/fs/smb/server/notify.h
index 8de46e07b02e..1132b91c1d54 100644
--- a/fs/smb/server/notify.h
+++ b/fs/smb/server/notify.h
@@ -14,11 +14,13 @@
 #define __SMB_SERVER_NOTIFY_H__
 
 struct ksmbd_work;
+struct ksmbd_file;
 struct smb2_change_notify_req;
 struct smb2_change_notify_rsp;
 
 int ksmbd_handle_notify(struct ksmbd_work *work,
 			struct smb2_change_notify_req *req,
 			struct smb2_change_notify_rsp *rsp);
+void ksmbd_notify_remove(struct ksmbd_file *fp);
 
 #endif /* __SMB_SERVER_NOTIFY_H__ */
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index a96b764c4db5..b242babf5771 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -18,6 +18,7 @@
 #include "vfs.h"
 #include "connection.h"
 #include "misc.h"
+#include "notify.h"
 #include "mgmt/tree_connect.h"
 #include "mgmt/user_session.h"
 #include "mgmt/user_config.h"
@@ -626,6 +627,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
 	close_id_del_oplock(fp);
 	filp = fp->filp;
 
+	ksmbd_notify_remove(fp);
 	__ksmbd_inode_close(fp);
 	if (!IS_ERR_OR_NULL(filp))
 		fput(filp);
@@ -1220,6 +1222,7 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
 	INIT_LIST_HEAD(&fp->node);
 	INIT_LIST_HEAD(&fp->lock_list);
 	spin_lock_init(&fp->f_lock);
+	mutex_init(&fp->notify_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 732ae26dd6a7..fe8a01b062cd 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -33,6 +33,7 @@
 #define SMB2_NO_FID		(0xFFFFFFFFFFFFFFFFULL)
 
 struct ksmbd_conn;
+struct ksmbd_notify;
 struct ksmbd_session;
 
 struct ksmbd_lock {
@@ -96,6 +97,9 @@ struct ksmbd_file {
 	u64				durable_volatile_id;
 
 	spinlock_t			f_lock;
+	/* Protects notify watch creation and removal. */
+	struct mutex			notify_lock;
+	struct ksmbd_notify		*notify;
 
 	struct ksmbd_inode		*f_ci;
 	struct ksmbd_inode		*f_parent_ci;
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 07/12] smb/server: save simple notify events
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (5 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 06/12] smb/server: keep notify watches on file handles ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-27 10:31   ` Namjae Jeon
  2026-09-26  9:05 ` [PATCH 08/12] smb/server: save old names for rename " ChenXiaoSong
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Save create, delete, and change events.

Example:

  1. client: smbinfo notify /mnt
  2. server: ksmbd.control --debug=notify
  3. server: touch /export/file
     server debug log:
       ksmbd: Queueing notify event, action 1, name file
       ksmbd: Queueing notify event, action 3, name file
  4. server: rm /export/file
     server debug log:
       ksmbd: Queueing notify event, action 2, name file

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 105 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index b49c0ec5117d..355370c66de0 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -22,6 +22,14 @@
 #include "vfs_cache.h"
 #include "mgmt/user_session.h"
 
+struct ksmbd_notify_event {
+	struct list_head list;
+	u32 action;
+	u64 when;
+	size_t name_len;
+	char name[];
+};
+
 struct ksmbd_notify {
 	struct fsnotify_group *group;
 	struct fsnotify_mark *mark;
@@ -29,6 +37,8 @@ struct ksmbd_notify {
 	struct ksmbd_file *fp;
 	/* Protects filter, rename state and the queued events. */
 	spinlock_t lock;
+	struct list_head events;
+	unsigned int num_events;
 	u32 filter;
 	u32 mask;
 	bool watch_tree;
@@ -40,6 +50,8 @@ struct ksmbd_notify_req {
 
 #define KSMBD_NOTIFY_NAME_EVENT_MASK	(FS_CREATE | FS_DELETE | \
 					 FS_MOVED_FROM | FS_MOVED_TO)
+#define KSMBD_NOTIFY_EVENT_MASK		(FS_ATTRIB | FS_MODIFY | \
+					 KSMBD_NOTIFY_NAME_EVENT_MASK)
 
 static const struct {
 	u32 notify_mask;
@@ -75,6 +87,34 @@ static u32 ksmbd_notify_map(u32 filter)
 	return mask;
 }
 
+static void ksmbd_notify_free_events(struct list_head *events)
+{
+	struct ksmbd_notify_event *event, *tmp;
+
+	list_for_each_entry_safe(event, tmp, events, list) {
+		list_del(&event->list);
+		kfree(event);
+	}
+}
+
+static bool ksmbd_notify_filter_match(struct ksmbd_notify *notify, u32 mask)
+{
+	u32 filter, notify_mask;
+
+	spin_lock(&notify->lock);
+	filter = notify->filter;
+	notify_mask = notify->mask;
+	spin_unlock(&notify->lock);
+
+	if (mask & KSMBD_NOTIFY_NAME_EVENT_MASK) {
+		if (mask & FS_ISDIR)
+			return filter & FILE_NOTIFY_CHANGE_DIR_NAME;
+		return filter & FILE_NOTIFY_CHANGE_FILE_NAME;
+	}
+
+	return mask & notify_mask;
+}
+
 /*
  * Cancel handler for a pending CHANGE_NOTIFY. Called either by
  * smb2_cancel() (conn->request_lock held, work->state already set to
@@ -94,14 +134,54 @@ static void smb2_notify_cancel(void **argv)
 	wake_up(&notify_req->wait);
 }
 
+static struct ksmbd_notify_event *
+ksmbd_notify_alloc_event(u32 action, const struct qstr *file_name, gfp_t gfp)
+{
+	struct ksmbd_notify_event *event;
+	size_t name_len = file_name ? file_name->len : 0;
+
+	event = kmalloc(sizeof(*event) + name_len + 1, gfp);
+	if (!event) {
+		pr_err("Failed to allocate notify event, action %u, name %.*s\n",
+		       action, file_name ? file_name->len : 0,
+		       file_name ? (const char *)file_name->name : "");
+		return NULL;
+	}
+
+	INIT_LIST_HEAD(&event->list);
+	event->action = action;
+	event->when = ktime_get_ns();
+	event->name_len = name_len;
+	if (name_len)
+		memcpy(event->name, file_name->name, name_len);
+	event->name[name_len] = '\0';
+
+	return event;
+}
+
+static void
+ksmbd_notify_queue_event(struct ksmbd_notify *notify,
+			 struct ksmbd_notify_event *event)
+{
+	ksmbd_debug(NOTIFY, "Queueing notify event, action %u, name %s\n",
+		    event->action, event->name);
+
+	spin_lock(&notify->lock);
+	list_add_tail(&event->list, &notify->events);
+	notify->num_events++;
+	spin_unlock(&notify->lock);
+}
+
 static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 					   u32 mask, struct inode *inode,
 					   struct inode *dir,
 					   const struct qstr *file_name,
 					   u32 cookie)
 {
+	struct ksmbd_notify_event *event;
 	struct inode *event_inode = dir ?: inode;
 	struct ksmbd_file *fp;
+	u32 action;
 
 	fp = notify->fp;
 
@@ -112,6 +192,29 @@ static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 		    file_name ? file_name->len : 0,
 		    file_name ? (const char *)file_name->name : "", cookie);
 
+	if (!(mask & KSMBD_NOTIFY_EVENT_MASK)) {
+		ksmbd_debug(NOTIFY, "Ignored notify event mask 0x%x\n", mask);
+		return 0;
+	}
+
+	if (mask & FS_CREATE) {
+		action = FILE_ACTION_ADDED;
+	} else if (mask & FS_DELETE) {
+		action = FILE_ACTION_REMOVED;
+	} else {
+		action = FILE_ACTION_MODIFIED;
+	}
+
+	if (!ksmbd_notify_filter_match(notify, mask))
+		return 0;
+
+	event = ksmbd_notify_alloc_event(action, file_name,
+					 KSMBD_DEFAULT_GFP);
+	if (!event)
+		return 0;
+
+	ksmbd_notify_queue_event(notify, event);
+
 	return 0;
 }
 
@@ -326,6 +429,7 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 
 	notify->fp = fp;
 	spin_lock_init(&notify->lock);
+	INIT_LIST_HEAD(&notify->events);
 	notify->filter = filter;
 	notify->mask = mask;
 	notify->watch_tree = watch_tree;
@@ -403,6 +507,7 @@ void ksmbd_notify_remove(struct ksmbd_file *fp)
 		    (unsigned long long)file_inode(fp->filp)->i_ino,
 		    notify->mark->mask, notify->watch_tree);
 	ksmbd_notify_destroy_marks(notify);
+	ksmbd_notify_free_events(&notify->events);
 	kfree(notify);
 }
 
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 08/12] smb/server: save old names for rename notify events
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (6 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 07/12] smb/server: save simple notify events ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 09/12] smb/server: match " ChenXiaoSong
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Save the old name from FS_MOVED_FROM events for matching with a subsequent
FS_MOVED_TO event. Queue unmatched events as removals after a short delay.

Example:

  1. client: smbinfo notify /mnt
  2. server: ksmbd.control --debug=notify
  3. server: mkdir /export/dir/; touch /export/file
  4. server: mv /export/file /export/dir/
     server debug log:
       [40622.865858] ksmbd: Saved moved from, mask 0x40, name file, cookie 1134
       [40622.975906] ksmbd: Queue moved from as removed, name file

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 76 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index 355370c66de0..ab1867033f26 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -42,12 +42,17 @@ struct ksmbd_notify {
 	u32 filter;
 	u32 mask;
 	bool watch_tree;
+	struct ksmbd_notify_event *moved_from_event;
+	u32 moved_from_mask;
+	u32 moved_from_cookie;
+	struct delayed_work moved_from_work;
 };
 
 struct ksmbd_notify_req {
 	wait_queue_head_t wait;
 };
 
+#define KSMBD_NOTIFY_MOVED_FROM_MSECS	100
 #define KSMBD_NOTIFY_NAME_EVENT_MASK	(FS_CREATE | FS_DELETE | \
 					 FS_MOVED_FROM | FS_MOVED_TO)
 #define KSMBD_NOTIFY_EVENT_MASK		(FS_ATTRIB | FS_MODIFY | \
@@ -172,6 +177,65 @@ ksmbd_notify_queue_event(struct ksmbd_notify *notify,
 	spin_unlock(&notify->lock);
 }
 
+static void
+ksmbd_notify_trigger_removed(struct ksmbd_notify *notify,
+			     struct ksmbd_notify_event *event)
+{
+	ksmbd_debug(NOTIFY,
+		    "Queue moved from as removed, name %s\n",
+		    event->name);
+	event->action = FILE_ACTION_REMOVED;
+	event->when = ktime_get_ns();
+	ksmbd_notify_queue_event(notify, event);
+}
+
+static void ksmbd_notify_moved_from_timeout(struct work_struct *work)
+{
+	struct ksmbd_notify_event *event;
+	struct ksmbd_notify *notify;
+
+	notify = container_of(to_delayed_work(work), struct ksmbd_notify,
+			      moved_from_work);
+
+	spin_lock(&notify->lock);
+	event = notify->moved_from_event;
+	notify->moved_from_event = NULL;
+	spin_unlock(&notify->lock);
+
+	if (!event)
+		return;
+	ksmbd_notify_trigger_removed(notify, event);
+}
+
+static void ksmbd_notify_save_moved_from(struct ksmbd_notify *notify,
+					 u32 mask, u32 cookie,
+					 const struct qstr *file_name)
+{
+	struct ksmbd_notify_event *event, *old_event;
+
+	event = ksmbd_notify_alloc_event(FILE_ACTION_RENAMED_OLD_NAME,
+					 file_name, KSMBD_DEFAULT_GFP);
+	if (!event)
+		return;
+
+	spin_lock(&notify->lock);
+	old_event = notify->moved_from_event;
+	notify->moved_from_event = event;
+	notify->moved_from_mask = mask;
+	notify->moved_from_cookie = cookie;
+	spin_unlock(&notify->lock);
+
+	ksmbd_debug(NOTIFY,
+		    "Saved moved from, mask 0x%x, name %.*s, cookie %u\n",
+		    mask, file_name ? file_name->len : 0,
+		    file_name ? (const char *)file_name->name : "", cookie);
+	if (old_event)
+		ksmbd_notify_trigger_removed(notify, old_event);
+
+	mod_delayed_work(system_dfl_wq, &notify->moved_from_work,
+			 msecs_to_jiffies(KSMBD_NOTIFY_MOVED_FROM_MSECS));
+}
+
 static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 					   u32 mask, struct inode *inode,
 					   struct inode *dir,
@@ -197,6 +261,14 @@ static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 		return 0;
 	}
 
+	if (mask & FS_MOVED_FROM) {
+		ksmbd_notify_save_moved_from(notify, mask, cookie, file_name);
+		return 0;
+	}
+
+	if (mask & FS_MOVED_TO)
+		return 0;
+
 	if (mask & FS_CREATE) {
 		action = FILE_ACTION_ADDED;
 	} else if (mask & FS_DELETE) {
@@ -433,6 +505,8 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	notify->filter = filter;
 	notify->mask = mask;
 	notify->watch_tree = watch_tree;
+	INIT_DELAYED_WORK(&notify->moved_from_work,
+			  ksmbd_notify_moved_from_timeout);
 
 	notify->group = fsnotify_alloc_group(&ksmbd_notify_fsnotify_ops, 0);
 	if (IS_ERR(notify->group)) {
@@ -507,6 +581,8 @@ void ksmbd_notify_remove(struct ksmbd_file *fp)
 		    (unsigned long long)file_inode(fp->filp)->i_ino,
 		    notify->mark->mask, notify->watch_tree);
 	ksmbd_notify_destroy_marks(notify);
+	cancel_delayed_work_sync(&notify->moved_from_work);
+	kfree(notify->moved_from_event);
 	ksmbd_notify_free_events(&notify->events);
 	kfree(notify);
 }
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 09/12] smb/server: match rename notify events
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (7 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 08/12] smb/server: save old names for rename " ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 10/12] smb/server: encode " ChenXiaoSong
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Match old and new rename names by cookie. Save both names as events.

Example:

  1. client: smbinfo notify /mnt
  2. server: ksmbd.control --debug=notify
  3. server: touch /export/file1
  4. server: mv /export/file1 /export/file2
     server debug log:
       ksmbd: fid 5:5, notify event: mask=0x00000040 inode=2 name=file1 cookie=974
       ksmbd: Saved moved from, mask 0x40, name file1, cookie 974
       ksmbd: fid 5:5, notify event: mask=0x00000080 inode=2 name=file2 cookie=974
       ksmbd: Matched rename file1 to file2, cookie 974
       ksmbd: Queueing notify event, action 4, name file1
       ksmbd: Queueing notify event, action 5, name file2

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 71 +++++++++++++++++++++++++++++++++++++++---
 1 file changed, 67 insertions(+), 4 deletions(-)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index ab1867033f26..6b5312b46e39 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -236,6 +236,67 @@ static void ksmbd_notify_save_moved_from(struct ksmbd_notify *notify,
 			 msecs_to_jiffies(KSMBD_NOTIFY_MOVED_FROM_MSECS));
 }
 
+static bool
+ksmbd_notify_handle_rename(struct ksmbd_notify *notify, u32 mask,
+				 u32 cookie, const struct qstr *file_name)
+{
+	struct ksmbd_notify_event *from, *to;
+	u32 from_mask;
+
+	to = ksmbd_notify_alloc_event(FILE_ACTION_RENAMED_NEW_NAME, file_name,
+				      KSMBD_DEFAULT_GFP);
+	if (!to)
+		return false;
+
+	spin_lock(&notify->lock);
+	if (!notify->moved_from_event ||
+	    notify->moved_from_cookie != cookie) {
+		spin_unlock(&notify->lock);
+		kfree(to);
+		ksmbd_debug(NOTIFY,
+			    "No rename source for destination %.*s, cookie %u\n",
+			    file_name ? file_name->len : 0,
+			    file_name ? (const char *)file_name->name : "",
+			    cookie);
+		return false;
+	}
+	from = notify->moved_from_event;
+	from_mask = notify->moved_from_mask;
+	notify->moved_from_event = NULL;
+	notify->moved_from_mask = 0;
+	notify->moved_from_cookie = 0;
+	cancel_delayed_work(&notify->moved_from_work);
+	spin_unlock(&notify->lock);
+
+	from->action = FILE_ACTION_RENAMED_OLD_NAME;
+	ksmbd_debug(NOTIFY, "Matched rename %.*s to %.*s, cookie %u\n",
+		    (int)from->name_len, from->name,
+		    file_name ? file_name->len : 0,
+		    file_name ? (const char *)file_name->name : "", cookie);
+
+	if (!ksmbd_notify_filter_match(notify, from_mask)) {
+		ksmbd_debug(NOTIFY, "Filtered rename source %.*s\n",
+			    (int)from->name_len, from->name);
+		kfree(from);
+		from = NULL;
+	}
+
+	if (!ksmbd_notify_filter_match(notify, mask)) {
+		ksmbd_debug(NOTIFY, "Filtered rename destination %.*s\n",
+			    file_name ? file_name->len : 0,
+			    file_name ? (const char *)file_name->name : "");
+		kfree(to);
+		to = NULL;
+	}
+
+	if (from)
+		ksmbd_notify_queue_event(notify, from);
+	if (to)
+		ksmbd_notify_queue_event(notify, to);
+
+	return true;
+}
+
 static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 					   u32 mask, struct inode *inode,
 					   struct inode *dir,
@@ -266,10 +327,12 @@ static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 		return 0;
 	}
 
-	if (mask & FS_MOVED_TO)
-		return 0;
-
-	if (mask & FS_CREATE) {
+	if (mask & FS_MOVED_TO) {
+		if (ksmbd_notify_handle_rename(notify, mask, cookie,
+					       file_name))
+			return 0;
+		action = FILE_ACTION_ADDED;
+	} else if (mask & FS_CREATE) {
 		action = FILE_ACTION_ADDED;
 	} else if (mask & FS_DELETE) {
 		action = FILE_ACTION_REMOVED;
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 10/12] smb/server: encode notify events
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (8 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 09/12] smb/server: match " ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 11/12] smb/server: send notify events to the client ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 12/12] smb/server: break directory leases before sending notify events ChenXiaoSong
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Sort and merge events, then encode their names as UTF-16 records.

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 106 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index 6b5312b46e39..da30cc058d78 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -12,6 +12,7 @@
 
 #include <linux/fsnotify_backend.h>
 #include <linux/dcache.h>
+#include <linux/list_sort.h>
 #include "glob.h"
 #include "../common/smb2status.h"
 #include "connection.h"
@@ -650,6 +651,111 @@ void ksmbd_notify_remove(struct ksmbd_file *fp)
 	kfree(notify);
 }
 
+static int ksmbd_notify_event_cmp(void *priv, const struct list_head *a,
+				  const struct list_head *b)
+{
+	struct ksmbd_notify_event *event_a;
+	struct ksmbd_notify_event *event_b;
+
+	event_a = list_entry(a, struct ksmbd_notify_event, list);
+	event_b = list_entry(b, struct ksmbd_notify_event, list);
+
+	if (event_a->when < event_b->when)
+		return -1;
+	if (event_a->when > event_b->when)
+		return 1;
+	return 0;
+}
+
+static void *ksmbd_notify_encode_events(struct ksmbd_work *work,
+					struct list_head *events,
+					u32 max_len, size_t *data_len)
+{
+	struct ksmbd_notify_event *event;
+	u8 *data = NULL;
+	size_t len = 0;
+
+	list_sort(NULL, events, ksmbd_notify_event_cmp);
+
+	list_for_each_entry(event, events, list) {
+		struct file_notify_information *info;
+		struct ksmbd_notify_event *next;
+		size_t alloc_len, buf_len, name_buf_len, record_len;
+		bool last = list_is_last(&event->list, events);
+		__le16 *name;
+		u8 *new_data;
+		int name_len;
+
+		/* Coalesce adjacent, case-sensitive duplicate records. */
+		if (!last) {
+			next = list_next_entry(event, list);
+			if (event->action == next->action &&
+			    event->name_len == next->name_len &&
+			    !memcmp(event->name, next->name, event->name_len))
+				continue;
+		}
+
+		name_buf_len = (event->name_len + 1) * sizeof(__le16);
+		name = kmalloc(name_buf_len, KSMBD_DEFAULT_GFP);
+		if (!name) {
+			pr_err("Failed to allocate notify event name buffer\n");
+			goto fail;
+		}
+
+		name_len = smbConvertToUTF16(name, event->name,
+					     event->name_len,
+					     work->conn->local_nls, 0);
+		name_len *= sizeof(__le16);
+		record_len = sizeof(*info) + name_len;
+		alloc_len = ALIGN(record_len, 4);
+		if (alloc_len > SIZE_MAX - 7 ||
+		    len > SIZE_MAX - alloc_len - 7) {
+			pr_err("Notify event data length overflow\n");
+			kfree(name);
+			goto fail;
+		}
+
+		/*
+		 * Compound response finalization can extend the last iov to an
+		 * 8-byte boundary.  Keep that padding inside this allocation and
+		 * zeroed, while reporting only the protocol payload in data_len.
+		 */
+		buf_len = ALIGN(len + alloc_len, 8);
+		new_data = kvrealloc(data, buf_len, KSMBD_DEFAULT_GFP);
+		if (!new_data) {
+			pr_err("Failed to allocate notify event data, length %zu\n",
+			       buf_len);
+			kfree(name);
+			goto fail;
+		}
+		data = new_data;
+		memset(data + len, 0, buf_len - len);
+
+		info = (struct file_notify_information *)(data + len);
+		info->NextEntryOffset = last ? 0 : cpu_to_le32(alloc_len);
+		info->Action = cpu_to_le32(event->action);
+		info->FileNameLength = cpu_to_le32(name_len);
+		memcpy(info->FileName, name, name_len);
+		kfree(name);
+
+		len += alloc_len;
+		if (len > max_len) {
+			ksmbd_debug(NOTIFY,
+				    "Notify event data length %zu exceeds output buffer %u\n",
+				    len, max_len);
+			goto fail;
+		}
+	}
+
+	*data_len = len;
+	return data;
+
+fail:
+	kvfree(data);
+	*data_len = 0;
+	return NULL;
+}
+
 static struct ksmbd_file *
 ksmbd_notify_validate_req(struct ksmbd_work *work,
 			  struct smb2_change_notify_req *req,
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 11/12] smb/server: send notify events to the client
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (9 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 10/12] smb/server: encode " ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  2026-09-26  9:05 ` [PATCH 12/12] smb/server: break directory leases before sending notify events ChenXiaoSong
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Wake one pending request and send its saved events in the reply.

Example:

  smbinfo notify /mnt

  # server: touch /export/file1
  Notifications received, returned data_len is 48
  Action: 0x00000001, FileName: file1
  Action: 0x00000003, FileName: file1

  # server: mv /export/file1 /export/file2
  Notifications received, returned data_len is 48
  Action: 0x00000004, FileName: file1
  Action: 0x00000005, FileName: file2

Suggested-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 188 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 185 insertions(+), 3 deletions(-)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index da30cc058d78..dc62b5d6d17d 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -47,12 +47,18 @@ struct ksmbd_notify {
 	u32 moved_from_mask;
 	u32 moved_from_cookie;
 	struct delayed_work moved_from_work;
+	struct delayed_work broadcast_work;
 };
 
 struct ksmbd_notify_req {
 	wait_queue_head_t wait;
+	struct list_head events;
+	unsigned int num_events;
+	bool notified;
 };
 
+#define KSMBD_NOTIFY_BROADCAST_MSECS	1000
+#define KSMBD_NOTIFY_BROADCAST_MAX_EVENTS	100
 #define KSMBD_NOTIFY_MOVED_FROM_MSECS	100
 #define KSMBD_NOTIFY_NAME_EVENT_MASK	(FS_CREATE | FS_DELETE | \
 					 FS_MOVED_FROM | FS_MOVED_TO)
@@ -169,13 +175,28 @@ static void
 ksmbd_notify_queue_event(struct ksmbd_notify *notify,
 			 struct ksmbd_notify_event *event)
 {
+	unsigned long broadcast_delay = 0;
+	bool schedule_broadcast = false;
+
 	ksmbd_debug(NOTIFY, "Queueing notify event, action %u, name %s\n",
 		    event->action, event->name);
 
 	spin_lock(&notify->lock);
 	list_add_tail(&event->list, &notify->events);
 	notify->num_events++;
+	/* Start one timer per batch; reaching the limit flushes it early. */
+	if (notify->num_events == 1) {
+		broadcast_delay =
+			msecs_to_jiffies(KSMBD_NOTIFY_BROADCAST_MSECS);
+		schedule_broadcast = true;
+	} else if (notify->num_events >= KSMBD_NOTIFY_BROADCAST_MAX_EVENTS) {
+		schedule_broadcast = true;
+	}
 	spin_unlock(&notify->lock);
+
+	if (schedule_broadcast)
+		mod_delayed_work(system_dfl_long_wq, &notify->broadcast_work,
+				 broadcast_delay);
 }
 
 static void
@@ -298,6 +319,57 @@ ksmbd_notify_handle_rename(struct ksmbd_notify *notify, u32 mask,
 	return true;
 }
 
+/*
+ * Give queued events to the oldest pending notify request. The caller holds
+ * notify->lock so this lookup and transfer are atomic with synchronous takes.
+ */
+static bool ksmbd_notify_wake_waiter(struct ksmbd_notify *notify)
+{
+	struct ksmbd_notify_req *notify_req;
+	struct ksmbd_work *work;
+	bool found = false;
+
+	spin_lock(&notify->fp->f_lock);
+	list_for_each_entry(work, &notify->fp->blocked_works, fp_entry) {
+		if (READ_ONCE(work->state) != KSMBD_WORK_ACTIVE ||
+		    work->cancel_fn != smb2_notify_cancel ||
+		    !work->cancel_argv)
+			continue;
+
+		notify_req = work->cancel_argv[0];
+		if (READ_ONCE(notify_req->notified))
+			continue;
+
+		list_splice_tail_init(&notify->events, &notify_req->events);
+		notify_req->num_events = notify->num_events;
+		notify->num_events = 0;
+		WRITE_ONCE(notify_req->notified, true);
+		wake_up(&notify_req->wait);
+		found = true;
+		break;
+	}
+	spin_unlock(&notify->fp->f_lock);
+
+	return found;
+}
+
+static void ksmbd_notify_broadcast(struct ksmbd_notify *notify)
+{
+	spin_lock(&notify->lock);
+	if (!list_empty(&notify->events))
+		ksmbd_notify_wake_waiter(notify);
+	spin_unlock(&notify->lock);
+}
+
+static void ksmbd_notify_broadcast_work(struct work_struct *work)
+{
+	struct ksmbd_notify *notify;
+
+	notify = container_of(to_delayed_work(work), struct ksmbd_notify,
+			      broadcast_work);
+	ksmbd_notify_broadcast(notify);
+}
+
 static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
 					   u32 mask, struct inode *inode,
 					   struct inode *dir,
@@ -571,6 +643,8 @@ static int ksmbd_notify_add(struct ksmbd_file *fp, u32 mask, u32 filter,
 	notify->watch_tree = watch_tree;
 	INIT_DELAYED_WORK(&notify->moved_from_work,
 			  ksmbd_notify_moved_from_timeout);
+	INIT_DELAYED_WORK(&notify->broadcast_work,
+			  ksmbd_notify_broadcast_work);
 
 	notify->group = fsnotify_alloc_group(&ksmbd_notify_fsnotify_ops, 0);
 	if (IS_ERR(notify->group)) {
@@ -646,11 +720,51 @@ void ksmbd_notify_remove(struct ksmbd_file *fp)
 		    notify->mark->mask, notify->watch_tree);
 	ksmbd_notify_destroy_marks(notify);
 	cancel_delayed_work_sync(&notify->moved_from_work);
+	cancel_delayed_work_sync(&notify->broadcast_work);
 	kfree(notify->moved_from_event);
 	ksmbd_notify_free_events(&notify->events);
 	kfree(notify);
 }
 
+static unsigned int
+ksmbd_notify_take_events(struct ksmbd_notify *notify, struct list_head *events)
+{
+	unsigned int num_events;
+
+	spin_lock(&notify->lock);
+	num_events = notify->num_events;
+	if (num_events && ksmbd_notify_wake_waiter(notify)) {
+		num_events = 0;
+	} else if (num_events) {
+		list_splice_tail_init(&notify->events, events);
+		notify->num_events = 0;
+	}
+	spin_unlock(&notify->lock);
+
+	if (num_events)
+		ksmbd_debug(NOTIFY,
+			    "Take %u queued notify events for synchronous reply\n",
+			    num_events);
+	return num_events;
+}
+
+static void
+ksmbd_notify_requeue_events(struct ksmbd_notify *notify,
+			    struct ksmbd_notify_req *notify_req)
+{
+	if (!notify_req->num_events)
+		return;
+
+	spin_lock(&notify->lock);
+	/* These events happened before any events already on the queue. */
+	list_splice_init(&notify_req->events, &notify->events);
+	notify->num_events += notify_req->num_events;
+	notify_req->num_events = 0;
+	spin_unlock(&notify->lock);
+
+	ksmbd_notify_broadcast(notify);
+}
+
 static int ksmbd_notify_event_cmp(void *priv, const struct list_head *a,
 				  const struct list_head *b)
 {
@@ -756,6 +870,48 @@ static void *ksmbd_notify_encode_events(struct ksmbd_work *work,
 	return NULL;
 }
 
+static int ksmbd_notify_reply(struct ksmbd_work *work,
+			      struct smb2_change_notify_req *req,
+			      struct smb2_change_notify_rsp *rsp,
+			      struct list_head *events)
+{
+	u32 max_len = le32_to_cpu(req->OutputBufferLength);
+	size_t data_len = 0;
+	void *data;
+	int err;
+
+	data = ksmbd_notify_encode_events(work, events, max_len, &data_len);
+	ksmbd_notify_free_events(events);
+
+	/* Maps a successful zero-length notify reply to ENUM_DIR. */
+	if (!data_len) {
+		ksmbd_debug(NOTIFY,
+			    "Return notify enum directory, output buffer length %u\n",
+			    max_len);
+		rsp->hdr.Status = STATUS_NOTIFY_ENUM_DIR;
+		return 0;
+	}
+
+	rsp->StructureSize = cpu_to_le16(9);
+	rsp->OutputBufferOffset = cpu_to_le16(72);
+	rsp->OutputBufferLength = cpu_to_le32(data_len);
+	err = ksmbd_iov_pin_rsp_read(work, rsp,
+				     offsetof(struct smb2_change_notify_rsp, Buffer),
+				     data, data_len);
+	if (err) {
+		pr_err("Failed to pin notify response data, length %zu: %d\n",
+		       data_len, err);
+		kvfree(data);
+		rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
+	} else {
+		ksmbd_debug(NOTIFY,
+			    "Prepared notify response, data length %zu\n",
+			    data_len);
+	}
+
+	return err;
+}
+
 static struct ksmbd_file *
 ksmbd_notify_validate_req(struct ksmbd_work *work,
 			  struct smb2_change_notify_req *req,
@@ -876,11 +1032,15 @@ static int ksmbd_notify_wait(struct ksmbd_work *work,
 	spin_unlock(&fp->f_lock);
 	read_unlock(&work->sess->file_table.lock);
 
+	/* Close the race between the synchronous check and queuing the waiter. */
+	ksmbd_notify_broadcast(notify);
+
 	ksmbd_debug(NOTIFY, "Notify request pending, async id %d\n",
 		    work->async_id);
 	smb2_send_interim_resp(work, STATUS_PENDING);
 
 	err = wait_event_interruptible(notify_req->wait,
+				       READ_ONCE(notify_req->notified) ||
 				       READ_ONCE(work->state) !=
 					       KSMBD_WORK_ACTIVE);
 	if (err && READ_ONCE(work->state) == KSMBD_WORK_ACTIVE) {
@@ -916,6 +1076,7 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 	struct ksmbd_notify_req notify_req = {};
 	struct ksmbd_notify *notify = NULL;
 	struct ksmbd_file *fp = NULL;
+	LIST_HEAD(events);
 	void **argv = NULL;
 	bool async_work = false;
 	int err = 0;
@@ -934,6 +1095,12 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 		goto out;
 	}
 
+	/* Changes which arrived without a waiter are returned synchronously. */
+	if (ksmbd_notify_take_events(notify, &events)) {
+		err = ksmbd_notify_reply(work, req, rsp, &events);
+		goto out;
+	}
+
 	argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP);
 	if (!argv) {
 		pr_err("Failed to allocate notify cancel arguments\n");
@@ -942,6 +1109,7 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 		goto out;
 	}
 	init_waitqueue_head(&notify_req.wait);
+	INIT_LIST_HEAD(&notify_req.events);
 	argv[0] = &notify_req;
 
 	err = setup_async_work(work, smb2_notify_cancel, argv);
@@ -959,16 +1127,30 @@ int ksmbd_handle_notify(struct ksmbd_work *work,
 	}
 
 	if (work->state == KSMBD_WORK_CLOSED) {
+		ksmbd_notify_requeue_events(notify, &notify_req);
 		rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
 		ksmbd_debug(NOTIFY, "Notify handle closed, async id %d\n",
 			    work->async_id);
-	} else {
+		smb2_send_interim_resp(work, rsp->hdr.Status);
+		work->send_no_response = 1;
+	} else if (work->state == KSMBD_WORK_CANCELLED) {
+		ksmbd_notify_requeue_events(notify, &notify_req);
 		rsp->hdr.Status = STATUS_CANCELLED;
 		ksmbd_debug(NOTIFY, "Notify request cancelled, async id %d\n",
 			    work->async_id);
+		smb2_send_interim_resp(work, rsp->hdr.Status);
+		work->send_no_response = 1;
+	} else {
+		/* Complete the request using the AsyncId sent in STATUS_PENDING. */
+		rsp->hdr.Flags |= SMB2_FLAGS_ASYNC_COMMAND;
+		rsp->hdr.Id.AsyncId = cpu_to_le64(work->async_id);
+		err = ksmbd_notify_reply(work, req, rsp,
+					 &notify_req.events);
+		if (!err)
+			ksmbd_debug(NOTIFY,
+				    "Completed notify request, async id %d\n",
+				    work->async_id);
 	}
-	smb2_send_interim_resp(work, rsp->hdr.Status);
-	work->send_no_response = 1;
 
 out:
 	if (rsp->hdr.Status != STATUS_SUCCESS && !work->send_no_response)
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 12/12] smb/server: break directory leases before sending notify events
  2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
                   ` (10 preceding siblings ...)
  2026-09-26  9:05 ` [PATCH 11/12] smb/server: send notify events to the client ChenXiaoSong
@ 2026-09-26  9:05 ` ChenXiaoSong
  11 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-26  9:05 UTC (permalink / raw)
  To: linkinjeon, tom, senozhatsky, chenxiaosong; +Cc: linux-cifs, ChenXiaoSong

From: ChenXiaoSong <chenxiaosong@kylinos.cn>

Reproducer:

  1. ksmbd: `ksmbd.conf`:
     [global]
             smb2 leases = yes
  2. ksmbd: systemctl start ksmbd
  3. Windows 11 File Explorer:
     Mount the share and enter the top-level directory of the mount point.
  4. ksmbd: touch /export/file
  5. Windows 11 File Explorer:
     `file` does not appear in the top-level directory of the mount point.

Windows can keep directory data in a cache while it has a directory
lease. A file change made on the server does not use the SMB request
path, so the lease is not broken.

The server sends a notify event, but Windows may still use the old data.
Some new files are then not shown in File Explorer.

Signed-off-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
---
 fs/smb/server/notify.c | 34 +++++++++++++++++++++++++++++++---
 fs/smb/server/oplock.c | 40 ++++++++++++++++++++++++++++++++++++++++
 fs/smb/server/oplock.h |  1 +
 3 files changed, 72 insertions(+), 3 deletions(-)

diff --git a/fs/smb/server/notify.c b/fs/smb/server/notify.c
index dc62b5d6d17d..ff014f856ac9 100644
--- a/fs/smb/server/notify.c
+++ b/fs/smb/server/notify.c
@@ -18,6 +18,7 @@
 #include "connection.h"
 #include "ksmbd_work.h"
 #include "notify.h"
+#include "oplock.h"
 #include "smb_common.h"
 #include "smb2pdu.h"
 #include "vfs_cache.h"
@@ -361,13 +362,34 @@ static void ksmbd_notify_broadcast(struct ksmbd_notify *notify)
 	spin_unlock(&notify->lock);
 }
 
+static bool ksmbd_notify_events_pending(struct ksmbd_notify *notify)
+{
+	bool pending;
+
+	spin_lock(&notify->lock);
+	pending = !list_empty(&notify->events);
+	spin_unlock(&notify->lock);
+
+	return pending;
+}
+
+/* Invalidate directory caches before making the change visible to a client. */
+static void ksmbd_notify_dispatch(struct ksmbd_notify *notify)
+{
+	if (!ksmbd_notify_events_pending(notify))
+		return;
+
+	smb_break_dir_lease(notify->fp);
+	ksmbd_notify_broadcast(notify);
+}
+
 static void ksmbd_notify_broadcast_work(struct work_struct *work)
 {
 	struct ksmbd_notify *notify;
 
 	notify = container_of(to_delayed_work(work), struct ksmbd_notify,
 			      broadcast_work);
-	ksmbd_notify_broadcast(notify);
+	ksmbd_notify_dispatch(notify);
 }
 
 static int ksmbd_notify_handle_inode_event(struct ksmbd_notify *notify,
@@ -731,6 +753,12 @@ ksmbd_notify_take_events(struct ksmbd_notify *notify, struct list_head *events)
 {
 	unsigned int num_events;
 
+	if (!ksmbd_notify_events_pending(notify))
+		return 0;
+
+	/* This path bypasses broadcast_work, so break directory leases here. */
+	smb_break_dir_lease(notify->fp);
+
 	spin_lock(&notify->lock);
 	num_events = notify->num_events;
 	if (num_events && ksmbd_notify_wake_waiter(notify)) {
@@ -762,7 +790,7 @@ ksmbd_notify_requeue_events(struct ksmbd_notify *notify,
 	notify_req->num_events = 0;
 	spin_unlock(&notify->lock);
 
-	ksmbd_notify_broadcast(notify);
+	ksmbd_notify_dispatch(notify);
 }
 
 static int ksmbd_notify_event_cmp(void *priv, const struct list_head *a,
@@ -1033,7 +1061,7 @@ static int ksmbd_notify_wait(struct ksmbd_work *work,
 	read_unlock(&work->sess->file_table.lock);
 
 	/* Close the race between the synchronous check and queuing the waiter. */
-	ksmbd_notify_broadcast(notify);
+	ksmbd_notify_dispatch(notify);
 
 	ksmbd_debug(NOTIFY, "Notify request pending, async id %d\n",
 		    work->async_id);
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 1b8c3482d1e4..ec2ee2acca35 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -1526,6 +1526,46 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
 	ksmbd_inode_put(p_ci);
 }
 
+/**
+ * smb_break_dir_lease() - break leases when a directory changes
+ * @fp: open directory that changed
+ *
+ * Some directory changes do not go through the SMB request path. fsnotify
+ * reports these changes. Break the directory leases before sending the
+ * changes to the SMB client.
+ *
+ * This function can sleep while it waits for a reply from the client. Do not
+ * call it from the fsnotify callback.
+ */
+void smb_break_dir_lease(struct ksmbd_file *fp)
+{
+	struct ksmbd_inode *ci = fp->f_ci;
+	struct oplock_info *opinfo;
+	LIST_HEAD(brk_list);
+
+	down_read(&ci->m_lock);
+	list_for_each_entry(opinfo, &ci->m_op_list, op_entry) {
+		if (!opinfo->conn || !opinfo->is_lease ||
+		    !opinfo->o_lease->is_dir ||
+		    opinfo->o_lease->state == SMB2_LEASE_NONE_LE)
+			continue;
+
+		if (!atomic_inc_not_zero(&opinfo->refcount))
+			continue;
+
+		if (ksmbd_conn_releasing(opinfo->conn)) {
+			opinfo_put(opinfo);
+			continue;
+		}
+
+		if (oplock_break_add(&brk_list, opinfo))
+			opinfo_put(opinfo);
+	}
+	up_read(&ci->m_lock);
+
+	oplock_break_drain_none(&brk_list, ci);
+}
+
 void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp)
 {
 	struct oplock_info *opinfo;
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index b08d21758e07..72ebcbacd2be 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -137,6 +137,7 @@ int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci,
 void destroy_lease_table(struct ksmbd_conn *conn);
 void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
 				      struct lease_ctx_info *lctx);
+void smb_break_dir_lease(struct ksmbd_file *fp);
 void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp);
 int smb2_check_durable_oplock(struct ksmbd_conn *conn,
 			      struct ksmbd_share_config *share,
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 17+ messages in thread

* Re: [PATCH 07/12] smb/server: save simple notify events
  2026-09-26  9:05 ` [PATCH 07/12] smb/server: save simple notify events ChenXiaoSong
@ 2026-09-27 10:31   ` Namjae Jeon
  2026-09-28  1:27     ` ChenXiaoSong
  0 siblings, 1 reply; 17+ messages in thread
From: Namjae Jeon @ 2026-09-27 10:31 UTC (permalink / raw)
  To: ChenXiaoSong; +Cc: tom, senozhatsky, linux-cifs, ChenXiaoSong

> +static void
> +ksmbd_notify_queue_event(struct ksmbd_notify *notify,
> +                        struct ksmbd_notify_event *event)
> +{
> +       ksmbd_debug(NOTIFY, "Queueing notify event, action %u, name %s\n",
> +                   event->action, event->name);
> +
> +       spin_lock(&notify->lock);
> +       list_add_tail(&event->list, &notify->events);
> +       notify->num_events++;
Could we put a per-open limit on the memory used by notify->events?

> +       spin_unlock(&notify->lock);
> +}

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 04/12] smb/server: support non-recursive directory change watches
  2026-09-26  9:05 ` [PATCH 04/12] smb/server: support non-recursive directory change watches ChenXiaoSong
@ 2026-09-27 10:51   ` Namjae Jeon
  2026-09-28  0:38     ` ChenXiaoSong
  0 siblings, 1 reply; 17+ messages in thread
From: Namjae Jeon @ 2026-09-27 10:51 UTC (permalink / raw)
  To: ChenXiaoSong; +Cc: tom, senozhatsky, linux-cifs, ChenXiaoSong

> +static const struct {
> +       u32 notify_mask;
> +       u32 fsnotify_mask;
> +} ksmbd_notify_mapping[] = {
> +       { FILE_NOTIFY_CHANGE_FILE_NAME,
> +         KSMBD_NOTIFY_NAME_EVENT_MASK },
> +       { FILE_NOTIFY_CHANGE_DIR_NAME,
> +         KSMBD_NOTIFY_NAME_EVENT_MASK },
> +       { FILE_NOTIFY_CHANGE_ATTRIBUTES,
> +         FS_ATTRIB | FS_MOVED_FROM | FS_MOVED_TO | FS_MODIFY },
> +       { FILE_NOTIFY_CHANGE_SIZE, FS_MODIFY },
> +       { FILE_NOTIFY_CHANGE_LAST_WRITE, FS_ATTRIB },
> +       { FILE_NOTIFY_CHANGE_LAST_ACCESS, FS_ATTRIB },
Should we also map FILE_NOTIFY_CHANGE_LAST_WRITE to FS_MODIFY and
FILE_NOTIFY_CHANGE_LAST_ACCESS to FS_ACCESS, then add FS_ACCESS to
KSMBD_NOTIFY_EVENT_MASK? With a LAST_WRITE-only filter, ordinary
writes emit FS_MODIFY, but this watch subscribes only to FS_ATTRIB, so
the request can remain pending. The same issue applies to reads with a
LAST_ACCESS-only filter.
> +       { FILE_NOTIFY_CHANGE_EA, FS_ATTRIB },
> +       { FILE_NOTIFY_CHANGE_SECURITY, FS_ATTRIB },
> +};

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 04/12] smb/server: support non-recursive directory change watches
  2026-09-27 10:51   ` Namjae Jeon
@ 2026-09-28  0:38     ` ChenXiaoSong
  0 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-28  0:38 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: tom, senozhatsky, linux-cifs, ChenXiaoSong

Yes, makes sense. Thanks for your suggestion.

On 9/27/26 18:51, Namjae Jeon wrote:
> Should we also map FILE_NOTIFY_CHANGE_LAST_WRITE to FS_MODIFY and
> FILE_NOTIFY_CHANGE_LAST_ACCESS to FS_ACCESS, then add FS_ACCESS to
> KSMBD_NOTIFY_EVENT_MASK? With a LAST_WRITE-only filter, ordinary
> writes emit FS_MODIFY, but this watch subscribes only to FS_ATTRIB, so
> the request can remain pending. The same issue applies to reads with a
> LAST_ACCESS-only filter.

-- 
ChenXiaoSong <chenxiaosong@chenxiaosong.com>
Chinese Homepage: https://chenxiaosong.com
English Homepage: https://chenxiaosong.com/en


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 07/12] smb/server: save simple notify events
  2026-09-27 10:31   ` Namjae Jeon
@ 2026-09-28  1:27     ` ChenXiaoSong
  0 siblings, 0 replies; 17+ messages in thread
From: ChenXiaoSong @ 2026-09-28  1:27 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: tom, senozhatsky, linux-cifs, ChenXiaoSong, ChenXiaoSong

Okay, I will add a limit on the number of events in the next version.

On 9/27/26 18:31, Namjae Jeon wrote:
> Could we put a per-open limit on the memory used by notify->events?

-- 
ChenXiaoSong <chenxiaosong@chenxiaosong.com>
Chinese Homepage: https://chenxiaosong.com
English Homepage: https://chenxiaosong.com/en


^ permalink raw reply	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-09-28  1:27 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-26  9:05 [PATCH 00/12] smb/server: change notify support ChenXiaoSong
2026-09-26  9:05 ` [PATCH 01/12] smb/server: move change notify handling into notify.c ChenXiaoSong
2026-09-26  9:05 ` [PATCH 02/12] smb/server: add more validation for change notify requests ChenXiaoSong
2026-09-26  9:05 ` [PATCH 03/12] smb/server: add debug type for change notify ChenXiaoSong
2026-09-26  9:05 ` [PATCH 04/12] smb/server: support non-recursive directory change watches ChenXiaoSong
2026-09-27 10:51   ` Namjae Jeon
2026-09-28  0:38     ` ChenXiaoSong
2026-09-26  9:05 ` [PATCH 05/12] smb/server: support recursive " ChenXiaoSong
2026-09-26  9:05 ` [PATCH 06/12] smb/server: keep notify watches on file handles ChenXiaoSong
2026-09-26  9:05 ` [PATCH 07/12] smb/server: save simple notify events ChenXiaoSong
2026-09-27 10:31   ` Namjae Jeon
2026-09-28  1:27     ` ChenXiaoSong
2026-09-26  9:05 ` [PATCH 08/12] smb/server: save old names for rename " ChenXiaoSong
2026-09-26  9:05 ` [PATCH 09/12] smb/server: match " ChenXiaoSong
2026-09-26  9:05 ` [PATCH 10/12] smb/server: encode " ChenXiaoSong
2026-09-26  9:05 ` [PATCH 11/12] smb/server: send notify events to the client ChenXiaoSong
2026-09-26  9:05 ` [PATCH 12/12] smb/server: break directory leases before sending notify events ChenXiaoSong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox