From 9061075b4f0a897e25ce46c396698dd24f92f5cb Mon Sep 17 00:00:00 2001 From: Christophe JAILLET Date: Sat, 16 May 2026 11:08:28 +0200 Subject: [PATCH 01/16] net/9p/usbg: Constify struct configfs_item_operations 'struct configfs_item_operations' is not modified in this driver. Constifying this structure moves some data to a read-only section, so increases overall security, especially when the structure holds some function pointers. On a x86_64, with allmodconfig: Before: ====== text data bss dec hex filename 25167 9336 256 34759 87c7 net/9p/trans_usbg.o After: ===== text data bss dec hex filename 25231 9272 256 34759 87c7 net/9p/trans_usbg.o Signed-off-by: Christophe JAILLET Message-ID: <2478bdabd7d169a686879c049f11dc307b5debbd.1778922467.git.christophe.jaillet@wanadoo.fr> Signed-off-by: Dominique Martinet --- net/9p/trans_usbg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/9p/trans_usbg.c b/net/9p/trans_usbg.c index 1ce70338999c..419cda13a7b5 100644 --- a/net/9p/trans_usbg.c +++ b/net/9p/trans_usbg.c @@ -804,7 +804,7 @@ static void usb9pfs_attr_release(struct config_item *item) usb_put_function_instance(&usb9pfs_opts->func_inst); } -static struct configfs_item_operations usb9pfs_item_ops = { +static const struct configfs_item_operations usb9pfs_item_ops = { .release = usb9pfs_attr_release, }; From b4d71bea144550ff4a0917f8c4b06d4063eb27a6 Mon Sep 17 00:00:00 2001 From: Pierre Barre Date: Tue, 12 May 2026 13:20:31 +0000 Subject: [PATCH 02/16] 9p: use kvzalloc for readdir buffer The readdir buffer is sized to msize, so kzalloc() can fail under fragmentation with a page allocation failure in v9fs_alloc_rdir_buf() / v9fs_dir_readdir_dotl(). The buffer is only a response sink and is never pack_sg_list()'d, so kvzalloc() is safe for all transports, unlike the fcall buffers fixed in e21d451a82f3 ("9p: Use kvmalloc for message buffers on supported transports"). Signed-off-by: Pierre Barre Message-ID: <20260512132032.369281-1-pierre@barre.sh> Signed-off-by: Dominique Martinet --- fs/9p/vfs_dir.c | 2 +- net/9p/client.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/9p/vfs_dir.c b/fs/9p/vfs_dir.c index e0d34e4e9076..e82f60c1c854 100644 --- a/fs/9p/vfs_dir.c +++ b/fs/9p/vfs_dir.c @@ -70,7 +70,7 @@ static struct p9_rdir *v9fs_alloc_rdir_buf(struct file *filp, int buflen) struct p9_fid *fid = filp->private_data; if (!fid->rdir) - fid->rdir = kzalloc(sizeof(struct p9_rdir) + buflen, GFP_KERNEL); + fid->rdir = kvzalloc(sizeof(struct p9_rdir) + buflen, GFP_KERNEL); return fid->rdir; } diff --git a/net/9p/client.c b/net/9p/client.c index f0dcf252af7e..b7d947910037 100644 --- a/net/9p/client.c +++ b/net/9p/client.c @@ -765,7 +765,7 @@ static void p9_fid_destroy(struct p9_fid *fid) spin_lock_irqsave(&clnt->lock, flags); idr_remove(&clnt->fids, fid->fid); spin_unlock_irqrestore(&clnt->lock, flags); - kfree(fid->rdir); + kvfree(fid->rdir); kfree(fid); } From e661e17ddbed524b5fbda789a091b48b6b677067 Mon Sep 17 00:00:00 2001 From: Pierre Barre Date: Tue, 12 May 2026 13:20:32 +0000 Subject: [PATCH 03/16] 9p: invalidate readdir buffer on seek The per-fid readdir buffer (fid->rdir) is populated lazily and only refilled when fully drained (rdir->head == rdir->tail). userspace lseek() on a directory fd updates file->f_pos via generic_file_llseek() but does not touch the cached buffer, so the next getdents() iterates the stale cache and emits entries from the previous position instead of the one the caller asked for. Track the file position the cached data corresponds to in struct p9_rdir, and drop the cache on entry to iterate_shared when it no longer matches ctx->pos. The 9p protocol's Tread/Treaddir already take an arbitrary offset on every request, so a refill at the new position is always legal; no .llseek override or seek restriction is needed. Reported-by: Pierre Barre Link: https://lore.kernel.org/v9fs/496d10b9-40fe-4f81-8014-37497c37ff63@app.fastmail.com/ Signed-off-by: Pierre Barre Message-ID: <20260512132032.369281-2-pierre@barre.sh> Signed-off-by: Dominique Martinet --- fs/9p/vfs_dir.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fs/9p/vfs_dir.c b/fs/9p/vfs_dir.c index e82f60c1c854..323f85352f6a 100644 --- a/fs/9p/vfs_dir.c +++ b/fs/9p/vfs_dir.c @@ -27,6 +27,7 @@ * struct p9_rdir - readdir accounting * @head: start offset of current dirread buffer * @tail: end offset of current dirread buffer + * @offset: file position the data at @head corresponds to * @buf: dirread buffer * * private structure for keeping track of readdir @@ -36,6 +37,7 @@ struct p9_rdir { int head; int tail; + loff_t offset; uint8_t buf[]; }; @@ -102,6 +104,9 @@ static int v9fs_dir_readdir(struct file *file, struct dir_context *ctx) kvec.iov_base = rdir->buf; kvec.iov_len = buflen; + if (rdir->head < rdir->tail && rdir->offset != ctx->pos) + rdir->head = rdir->tail = 0; + while (1) { if (rdir->tail == rdir->head) { struct iov_iter to; @@ -117,6 +122,7 @@ static int v9fs_dir_readdir(struct file *file, struct dir_context *ctx) rdir->head = 0; rdir->tail = n; + rdir->offset = ctx->pos; } while (rdir->head < rdir->tail) { err = p9stat_read(fid->clnt, rdir->buf + rdir->head, @@ -134,6 +140,7 @@ static int v9fs_dir_readdir(struct file *file, struct dir_context *ctx) rdir->head += err; ctx->pos += err; + rdir->offset = ctx->pos; } } } @@ -161,6 +168,9 @@ static int v9fs_dir_readdir_dotl(struct file *file, struct dir_context *ctx) if (!rdir) return -ENOMEM; + if (rdir->head < rdir->tail && rdir->offset != ctx->pos) + rdir->head = rdir->tail = 0; + while (1) { if (rdir->tail == rdir->head) { err = p9_client_readdir(fid, rdir->buf, buflen, @@ -170,6 +180,7 @@ static int v9fs_dir_readdir_dotl(struct file *file, struct dir_context *ctx) rdir->head = 0; rdir->tail = err; + rdir->offset = ctx->pos; } while (rdir->head < rdir->tail) { @@ -190,6 +201,7 @@ static int v9fs_dir_readdir_dotl(struct file *file, struct dir_context *ctx) ctx->pos = curdirent.d_off; rdir->head += err; + rdir->offset = ctx->pos; } } } From 38ba49580e875786b85bad1d3895aa9b9f4b3431 Mon Sep 17 00:00:00 2001 From: Aayush Patil Date: Sun, 10 May 2026 23:58:56 +0530 Subject: [PATCH 04/16] docs/filesystems/9p: fix broken external links The xcpu.org links for xcpu-talk, kvmfs, and cellfs-talk are dead with no archived snapshots available on the Wayback Machine, so remove them. The PROSE I/O link redirects to a dead server; replace it with an archived version from web.archive.org. Signed-off-by: Aayush Patil Message-ID: <20260510182856.17569-1-aayushpatilsch@gmail.com> Signed-off-by: Dominique Martinet --- Documentation/filesystems/9p.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Documentation/filesystems/9p.rst b/Documentation/filesystems/9p.rst index be3504ca034a..65809a1dad21 100644 --- a/Documentation/filesystems/9p.rst +++ b/Documentation/filesystems/9p.rst @@ -23,13 +23,10 @@ the 9p client is available in the form of a USENIX paper: Other applications are described in the following papers: * XCPU & Clustering - http://xcpu.org/papers/xcpu-talk.pdf * KVMFS: control file system for KVM - http://xcpu.org/papers/kvmfs.pdf * CellFS: A New Programming Model for the Cell BE - http://xcpu.org/papers/cellfs-talk.pdf * PROSE I/O: Using 9p to enable Application Partitions - http://plan9.escet.urjc.es/iwp9/cready/PROSE_iwp9_2006.pdf + http://web.archive.org/web/20110101152020/http://plan9.escet.urjc.es/iwp9/cready/PROSE_iwp9_2006.pdf * VirtFS: A Virtualization Aware File System pass-through https://kernel.org/doc/ols/2010/ols2010-pages-109-120.pdf From 6b4f48728faa8bb514368f7eacda05565dea8696 Mon Sep 17 00:00:00 2001 From: Vasiliy Kovalev Date: Wed, 15 Apr 2026 18:52:37 +0300 Subject: [PATCH 05/16] net/9p: fix infinite loop in p9_client_rpc on fatal signal When p9_client_rpc() is called with type P9_TFLUSH and the transport has no peer (e.g. fd transport backed by pipes with no 9p server), a fatal signal causes an infinite loop: again: err = io_wait_event_killable(req->wq, ...) /* SIGKILL wakes the task, returns -ERESTARTSYS */ if (err == -ERESTARTSYS && c->status == Connected && type == P9_TFLUSH) { sigpending = 1; clear_thread_flag(TIF_SIGPENDING); goto again; } clear_thread_flag() clears TIF_SIGPENDING before jumping back to io_wait_event_killable(). signal_pending_state() checks TIF_SIGPENDING, finds it zero, and the task goes to sleep again. The task can only wake on the next signal delivery that calls signal_wake_up() and sets TIF_SIGPENDING again. When that happens the loop repeats, clears TIF_SIGPENDING, and sleeps again indefinitely. This is triggered in practice by coredump_wait(): when a thread in a multi-threaded process causes a coredump (e.g. via SIGSYS from Syscall User Dispatch), coredump_wait() sends SIGKILL to all other threads and waits for them to call mm_release(). If one of those threads is blocked in p9_client_rpc() over an fd transport with no peer, it enters the P9_TFLUSH loop and never calls mm_release(), so coredump_wait() stalls forever: INFO: task syz.0.18:676 blocked for more than 143 seconds. Not tainted 6.12.77+ #1 task:syz.0.18 state:D stack:27600 pid:676 tgid:673 ppid:630 flags:0x00000004 Call Trace: context_switch kernel/sched/core.c:5344 [inline] __schedule+0xcb4/0x5d50 kernel/sched/core.c:6724 __schedule_loop kernel/sched/core.c:6801 [inline] schedule+0xe5/0x350 kernel/sched/core.c:6816 schedule_timeout+0x253/0x290 kernel/time/timer.c:2593 do_wait_for_common kernel/sched/completion.c:95 [inline] __wait_for_common+0x409/0x600 kernel/sched/completion.c:116 wait_for_common kernel/sched/completion.c:127 [inline] wait_for_completion_state+0x1d/0x40 kernel/sched/completion.c:264 coredump_wait fs/coredump.c:448 [inline] do_coredump+0x854/0x4350 fs/coredump.c:629 get_signal+0x1425/0x2730 kernel/signal.c:2903 arch_do_signal_or_restart+0x81/0x880 arch/x86/kernel/signal.c:337 exit_to_user_mode_loop kernel/entry/common.c:111 [inline] exit_to_user_mode_prepare include/linux/entry-common.h:328 [inline] __syscall_exit_to_user_mode_work kernel/entry/common.c:207 [inline] syscall_exit_to_user_mode+0xf9/0x160 kernel/entry/common.c:218 do_syscall_64+0x102/0x220 arch/x86/entry/common.c:84 entry_SYSCALL_64_after_hwframe+0x77/0x7f Fix: check fatal_signal_pending() before clearing TIF_SIGPENDING in the P9_TFLUSH retry loop. At that point TIF_SIGPENDING is still set, so fatal_signal_pending() works correctly. If a fatal signal is pending, jump to recalc_sigpending to restore TIF_SIGPENDING and return -ERESTARTSYS to the caller. The same defect is present in stable kernels back to 5.4. On those kernels the infinite loop is broken earlier by a second SIGKILL from the parent process (e.g. kill_and_wait() retrying after a timeout), resulting in a zombie process and a shutdown delay rather than a permanent D-state hang, but the underlying flaw is the same. Found by Linux Verification Center (linuxtesting.org) with Syzkaller. Fixes: 91b8534fa8f5 ("9p: make rpc code common and rework flush code") Closes: https://syzkaller.appspot.com/bug?extid=3ce7863f8fc836a427e7 Cc: stable@vger.kernel.org Signed-off-by: Vasiliy Kovalev Message-ID: <20260415155237.182891-1-kovalev@altlinux.org> Signed-off-by: Dominique Martinet --- net/9p/client.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/net/9p/client.c b/net/9p/client.c index b7d947910037..69d3efd340c0 100644 --- a/net/9p/client.c +++ b/net/9p/client.c @@ -600,6 +600,8 @@ p9_client_rpc(struct p9_client *c, int8_t type, const char *fmt, ...) if (err == -ERESTARTSYS && c->status == Connected && type == P9_TFLUSH) { + if (fatal_signal_pending(current)) + goto recalc_sigpending; sigpending = 1; clear_thread_flag(TIF_SIGPENDING); goto again; From 1a3860d46e3eb47dbd60339783cdad7904486b9f Mon Sep 17 00:00:00 2001 From: Yizhou Zhao Date: Thu, 28 May 2026 13:39:16 +0800 Subject: [PATCH 06/16] 9p: avoid putting oldfid in p9_client_walk() error path When p9_client_walk() is called with clone set to false, fid aliases oldfid. If the walk subsequently fails after the request has been sent, the error path jumps to clunk_fid, which currently calls p9_fid_put(fid) unconditionally. This drops a reference to oldfid even though ownership of oldfid remains with the caller. If this is the last reference, oldfid can be clunked and destroyed while the caller still expects it to be valid. A later use or put of oldfid can then trigger a use-after-free or refcount underflow. Fix this by only putting fid in the clunk_fid error path when it does not alias oldfid, matching the existing guard in the error path below. This can be triggered when a multi-component walk is split into multiple p9_client_walk() calls and a later non-cloning walk fails. A reproducer and refcount warning logs are available on request. Fixes: b48dbb998d70 ("9p fid refcount: add p9_fid_get/put wrappers") Cc: stable@vger.kernel.org Reported-by: Yuxiang Yang Reported-by: Ao Wang Reported-by: Xuewei Feng Reported-by: Qi Li Reported-by: Ke Xu Assisted-by: GLM 5.1 Signed-off-by: Yizhou Zhao Message-ID: <20260528053918.53550-1-zhaoyz24@mails.tsinghua.edu.cn> Signed-off-by: Dominique Martinet --- net/9p/client.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/net/9p/client.c b/net/9p/client.c index 69d3efd340c0..ef64546c6d52 100644 --- a/net/9p/client.c +++ b/net/9p/client.c @@ -1094,7 +1094,8 @@ struct p9_fid *p9_client_walk(struct p9_fid *oldfid, uint16_t nwname, clunk_fid: kfree(wqids); - p9_fid_put(fid); + if (fid != oldfid) + p9_fid_put(fid); fid = NULL; error: From 314b58c01a9047567fd19446ca5fd46c473b89ff Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Wed, 20 May 2026 10:26:50 +0800 Subject: [PATCH 07/16] 9p: avoid returning ERR_PTR(0) from mkdir operations When mkdir succeeds, v9fs_vfs_mkdir_dotl() and v9fs_vfs_mkdir() return ERR_PTR(0) which is incorrect. They should return NULL instead for success and ERR_PTR() only with negative error codes for failure. Return NULL instead of passing to ERR_PTR while err is zero Fixes smatch warnings: fs/9p/vfs_inode_dotl.c:420 v9fs_vfs_mkdir_dotl() warn: passing zero to 'ERR_PTR' fs/9p/vfs_inode.c:695 v9fs_vfs_mkdir() warn: passing zero to 'ERR_PTR' The v9fs_vfs_mkdir() code was further simplified because v9fs_create() can never return NULL, so we do not need to check for fid being set separately, and the error path can be a simple return immediately after v9fs_create() failure. There is no intended functional change. Fixes: 88d5baf69082 ("Change inode_operations.mkdir to return struct dentry *") Suggested-by: David Laight Acked-by: Christian Schoenebeck Signed-off-by: Hongling Zeng Message-ID: <20260520022650.14217-1-zenghongling@kylinos.cn> Signed-off-by: Dominique Martinet --- fs/9p/vfs_inode.c | 19 ++++++------------- fs/9p/vfs_inode_dotl.c | 4 ++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/fs/9p/vfs_inode.c b/fs/9p/vfs_inode.c index 97abe65bf7c1..e4b15ebba3aa 100644 --- a/fs/9p/vfs_inode.c +++ b/fs/9p/vfs_inode.c @@ -672,27 +672,20 @@ v9fs_vfs_create(struct mnt_idmap *idmap, struct inode *dir, static struct dentry *v9fs_vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { - int err; u32 perm; struct p9_fid *fid; struct v9fs_session_info *v9ses; p9_debug(P9_DEBUG_VFS, "name %pd\n", dentry); - err = 0; v9ses = v9fs_inode2v9ses(dir); perm = unixmode2p9mode(v9ses, mode | S_IFDIR); fid = v9fs_create(v9ses, dir, dentry, NULL, perm, P9_OREAD); - if (IS_ERR(fid)) { - err = PTR_ERR(fid); - fid = NULL; - } else { - inc_nlink(dir); - v9fs_invalidate_inode_attr(dir); - } - - if (fid) - p9_fid_put(fid); - return ERR_PTR(err); + if (IS_ERR(fid)) + return ERR_CAST(fid); + inc_nlink(dir); + v9fs_invalidate_inode_attr(dir); + p9_fid_put(fid); + return NULL; } /** diff --git a/fs/9p/vfs_inode_dotl.c b/fs/9p/vfs_inode_dotl.c index 643e759eacb2..fae324681ff3 100644 --- a/fs/9p/vfs_inode_dotl.c +++ b/fs/9p/vfs_inode_dotl.c @@ -349,7 +349,7 @@ static struct dentry *v9fs_vfs_mkdir_dotl(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t omode) { - int err; + int err = 0; struct v9fs_session_info *v9ses; struct p9_fid *fid = NULL, *dfid = NULL; kgid_t gid; @@ -412,7 +412,7 @@ static struct dentry *v9fs_vfs_mkdir_dotl(struct mnt_idmap *idmap, p9_fid_put(fid); v9fs_put_acl(dacl, pacl); p9_fid_put(dfid); - return ERR_PTR(err); + return err ? ERR_PTR(err) : NULL; } static int From f00e6c1d282578b077b87b88f42e701bc40f2fff Mon Sep 17 00:00:00 2001 From: Remi Pommarel Date: Thu, 21 May 2026 11:40:29 +0200 Subject: [PATCH 08/16] 9p: Cache negative dentries for lookup performance Not caching negative dentries can result in poor performance for workloads that repeatedly look up non-existent paths. Each such lookup triggers a full 9P transaction with the server, adding unnecessary overhead. A typical example is source compilation, where multiple cc1 processes are spawned and repeatedly search for the same missing header files over and over again. This change enables caching of negative dentries, so that lookups for known non-existent paths do not require a full 9P transaction. The cached negative dentries are retained for a configurable duration (expressed in milliseconds), as specified by the ndentry_timeout field in struct v9fs_session_info. If set to -1, negative dentries are cached indefinitely. This optimization reduces lookup overhead and improves performance for workloads involving frequent access to non-existent paths. Signed-off-by: Remi Pommarel Message-ID: Signed-off-by: Dominique Martinet --- fs/9p/fid.c | 11 +++-- fs/9p/v9fs.c | 1 + fs/9p/v9fs.h | 5 ++ fs/9p/v9fs_vfs.h | 15 ++++++ fs/9p/vfs_dentry.c | 105 ++++++++++++++++++++++++++++++++++------ fs/9p/vfs_inode.c | 12 +++-- fs/9p/vfs_super.c | 1 + include/net/9p/client.h | 2 + 8 files changed, 128 insertions(+), 24 deletions(-) diff --git a/fs/9p/fid.c b/fs/9p/fid.c index f84412290a30..76242d450aa7 100644 --- a/fs/9p/fid.c +++ b/fs/9p/fid.c @@ -20,7 +20,9 @@ static inline void __add_fid(struct dentry *dentry, struct p9_fid *fid) { - hlist_add_head(&fid->dlist, (struct hlist_head *)&dentry->d_fsdata); + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); + + hlist_add_head(&fid->dlist, &v9fs_dentry->head); } @@ -112,6 +114,7 @@ void v9fs_open_fid_add(struct inode *inode, struct p9_fid **pfid) static struct p9_fid *v9fs_fid_find(struct dentry *dentry, kuid_t uid, int any) { + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); struct p9_fid *fid, *ret; p9_debug(P9_DEBUG_VFS, " dentry: %pd (%p) uid %d any %d\n", @@ -119,11 +122,9 @@ static struct p9_fid *v9fs_fid_find(struct dentry *dentry, kuid_t uid, int any) any); ret = NULL; /* we'll recheck under lock if there's anything to look in */ - if (dentry->d_fsdata) { - struct hlist_head *h = (struct hlist_head *)&dentry->d_fsdata; - + if (!hlist_empty(&v9fs_dentry->head)) { spin_lock(&dentry->d_lock); - hlist_for_each_entry(fid, h, dlist) { + hlist_for_each_entry(fid, &v9fs_dentry->head, dlist) { if (any || uid_eq(fid->uid, uid)) { ret = fid; p9_fid_get(ret); diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c index acda42499ca9..be83744b75b2 100644 --- a/fs/9p/v9fs.c +++ b/fs/9p/v9fs.c @@ -426,6 +426,7 @@ static void v9fs_apply_options(struct v9fs_session_info *v9ses, v9ses->cache = ctx->session_opts.cache; v9ses->uid = ctx->session_opts.uid; v9ses->session_lock_timeout = ctx->session_opts.session_lock_timeout; + v9ses->ndentry_timeout_ms = ctx->session_opts.ndentry_timeout_ms; } /** diff --git a/fs/9p/v9fs.h b/fs/9p/v9fs.h index 6a12445d3858..e630c5111d74 100644 --- a/fs/9p/v9fs.h +++ b/fs/9p/v9fs.h @@ -91,6 +91,7 @@ enum p9_cache_bits { * @debug: debug level * @afid: authentication handle * @cache: cache mode of type &p9_cache_bits + * @ndentry_timeout: Negative dentry lookup cache retention time in ms * @cachetag: the tag of the cache associated with this session * @fscache: session cookie associated with FS-Cache * @uname: string user name to mount hierarchy as @@ -101,6 +102,7 @@ enum p9_cache_bits { * @uid: if %V9FS_ACCESS_SINGLE, the numeric uid which mounted the hierarchy * @clnt: reference to 9P network client instantiated for this session * @slist: reference to list of registered 9p sessions + * @ndentry_timeout_ms: Negative dentry caching retention time * * This structure holds state for each session instance established during * a sys_mount() . @@ -116,6 +118,7 @@ struct v9fs_session_info { unsigned short debug; unsigned int afid; unsigned int cache; + unsigned int ndentry_timeout_ms; #ifdef CONFIG_9P_FSCACHE char *cachetag; struct fscache_volume *fscache; @@ -133,6 +136,8 @@ struct v9fs_session_info { long session_lock_timeout; /* retry interval for blocking locks */ }; +#define NDENTRY_TIMEOUT_NEVER (-1U) + /* cache_validity flags */ #define V9FS_INO_INVALID_ATTR 0x01 diff --git a/fs/9p/v9fs_vfs.h b/fs/9p/v9fs_vfs.h index d3aefbec4de6..83c2335f438d 100644 --- a/fs/9p/v9fs_vfs.h +++ b/fs/9p/v9fs_vfs.h @@ -28,6 +28,19 @@ /* flags for v9fs_stat2inode() & v9fs_stat2inode_dotl() */ #define V9FS_STAT2INODE_KEEP_ISIZE 1 +/** + * struct v9fs_dentry - v9fs specific dentry data + * @head: List of fid associated with this dentry + * @expire_time: Lookup cache expiration time for negative dentries + * @rcu: used by kfree_rcu to schedule clean up job + */ +struct v9fs_dentry { + struct hlist_head head; + u64 expire_time; + struct rcu_head rcu; +}; +#define to_v9fs_dentry(d) ((struct v9fs_dentry *)((d)->d_fsdata)) + extern struct file_system_type v9fs_fs_type; extern const struct address_space_operations v9fs_addr_operations; extern const struct file_operations v9fs_file_operations; @@ -35,6 +48,8 @@ extern const struct file_operations v9fs_file_operations_dotl; extern const struct file_operations v9fs_dir_operations; extern const struct file_operations v9fs_dir_operations_dotl; extern const struct dentry_operations v9fs_dentry_operations; +extern void v9fs_ndentry_refresh_timeout(struct dentry *dentry); +extern void v9fs_dentry_fid_remove(struct dentry *dentry); extern const struct dentry_operations v9fs_cached_dentry_operations; extern struct kmem_cache *v9fs_inode_cache; diff --git a/fs/9p/vfs_dentry.c b/fs/9p/vfs_dentry.c index c5bf74d547e8..e549e222602e 100644 --- a/fs/9p/vfs_dentry.c +++ b/fs/9p/vfs_dentry.c @@ -23,6 +23,46 @@ #include "v9fs_vfs.h" #include "fid.h" +/** + * v9fs_ndentry_is_expired - Check if negative dentry lookup has expired + * + * This should be called to know if a negative dentry should be removed from + * cache. + * + * @dentry: dentry in question + * + */ +static bool v9fs_ndentry_is_expired(struct dentry const *dentry) +{ + struct v9fs_session_info *v9ses = v9fs_dentry2v9ses(dentry); + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); + + if (v9ses->ndentry_timeout_ms == NDENTRY_TIMEOUT_NEVER) + return false; + + return time_before_eq64(v9fs_dentry->expire_time, get_jiffies_64()); +} + +/** + * v9fs_ndentry_refresh_timeout - Refresh negative dentry lookup cache timeout + * + * This should be called when a look up yields a negative entry. + * + * @dentry: dentry in question + * + */ +void v9fs_ndentry_refresh_timeout(struct dentry *dentry) +{ + struct v9fs_session_info *v9ses = v9fs_dentry2v9ses(dentry); + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); + + if (v9ses->ndentry_timeout_ms == NDENTRY_TIMEOUT_NEVER) + return; + + v9fs_dentry->expire_time = get_jiffies_64() + + msecs_to_jiffies(v9ses->ndentry_timeout_ms); +} + /** * v9fs_cached_dentry_delete - called when dentry refcount equals 0 * @dentry: dentry in question @@ -33,20 +73,15 @@ static int v9fs_cached_dentry_delete(const struct dentry *dentry) p9_debug(P9_DEBUG_VFS, " dentry: %pd (%p)\n", dentry, dentry); - /* Don't cache negative dentries */ - if (d_really_is_negative(dentry)) - return 1; - return 0; + if (!d_really_is_negative(dentry)) + return 0; + + return v9fs_ndentry_is_expired(dentry); } -/** - * v9fs_dentry_release - called when dentry is going to be freed - * @dentry: dentry that is being release - * - */ - -static void v9fs_dentry_release(struct dentry *dentry) +static void __v9fs_dentry_fid_remove(struct dentry *dentry) { + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); struct hlist_node *p, *n; struct hlist_head head; @@ -54,13 +89,54 @@ static void v9fs_dentry_release(struct dentry *dentry) dentry, dentry); spin_lock(&dentry->d_lock); - hlist_move_list((struct hlist_head *)&dentry->d_fsdata, &head); + hlist_move_list(&v9fs_dentry->head, &head); spin_unlock(&dentry->d_lock); hlist_for_each_safe(p, n, &head) p9_fid_put(hlist_entry(p, struct p9_fid, dlist)); } +/** + * v9fs_dentry_fid_remove - Release all dentry's fids + * @dentry: dentry in question + * + */ +void v9fs_dentry_fid_remove(struct dentry *dentry) +{ + __v9fs_dentry_fid_remove(dentry); +} + +/** + * v9fs_dentry_init - Initialize v9fs dentry data + * @dentry: dentry in question + * + */ +static int v9fs_dentry_init(struct dentry *dentry) +{ + struct v9fs_dentry *v9fs_dentry = kzalloc(sizeof(*v9fs_dentry), + GFP_KERNEL); + + if (!v9fs_dentry) + return -ENOMEM; + + INIT_HLIST_HEAD(&v9fs_dentry->head); + dentry->d_fsdata = (void *)v9fs_dentry; + return 0; +} + +/** + * v9fs_dentry_release - called when dentry is going to be freed + * @dentry: dentry that is being released + * + */ +static void v9fs_dentry_release(struct dentry *dentry) +{ + struct v9fs_dentry *v9fs_dentry = to_v9fs_dentry(dentry); + + __v9fs_dentry_fid_remove(dentry); + kfree_rcu(v9fs_dentry, rcu); +} + static int __v9fs_lookup_revalidate(struct dentry *dentry, unsigned int flags) { struct p9_fid *fid; @@ -72,7 +148,7 @@ static int __v9fs_lookup_revalidate(struct dentry *dentry, unsigned int flags) inode = d_inode(dentry); if (!inode) - goto out_valid; + return !v9fs_ndentry_is_expired(dentry); v9inode = V9FS_I(inode); if (v9inode->cache_validity & V9FS_INO_INVALID_ATTR) { @@ -112,7 +188,6 @@ static int __v9fs_lookup_revalidate(struct dentry *dentry, unsigned int flags) return retval; } } -out_valid: p9_debug(P9_DEBUG_VFS, "dentry: %pd (%p) is valid\n", dentry, dentry); return 1; } @@ -139,12 +214,14 @@ const struct dentry_operations v9fs_cached_dentry_operations = { .d_revalidate = v9fs_lookup_revalidate, .d_weak_revalidate = __v9fs_lookup_revalidate, .d_delete = v9fs_cached_dentry_delete, + .d_init = v9fs_dentry_init, .d_release = v9fs_dentry_release, .d_unalias_trylock = v9fs_dentry_unalias_trylock, .d_unalias_unlock = v9fs_dentry_unalias_unlock, }; const struct dentry_operations v9fs_dentry_operations = { + .d_init = v9fs_dentry_init, .d_release = v9fs_dentry_release, .d_unalias_trylock = v9fs_dentry_unalias_trylock, .d_unalias_unlock = v9fs_dentry_unalias_unlock, diff --git a/fs/9p/vfs_inode.c b/fs/9p/vfs_inode.c index e4b15ebba3aa..a178e8cb2c82 100644 --- a/fs/9p/vfs_inode.c +++ b/fs/9p/vfs_inode.c @@ -549,7 +549,7 @@ static int v9fs_remove(struct inode *dir, struct dentry *dentry, int flags) /* invalidate all fids associated with dentry */ /* NOTE: This will not include open fids */ - dentry->d_op->d_release(dentry); + v9fs_dentry_fid_remove(dentry); } return retval; } @@ -725,14 +725,16 @@ struct dentry *v9fs_vfs_lookup(struct inode *dir, struct dentry *dentry, name = dentry->d_name.name; fid = p9_client_walk(dfid, 1, &name, 1); p9_fid_put(dfid); - if (fid == ERR_PTR(-ENOENT)) + if (fid == ERR_PTR(-ENOENT)) { inode = NULL; - else if (IS_ERR(fid)) + v9fs_ndentry_refresh_timeout(dentry); + } else if (IS_ERR(fid)) { inode = ERR_CAST(fid); - else if (v9ses->cache & (CACHE_META|CACHE_LOOSE)) + } else if (v9ses->cache & (CACHE_META|CACHE_LOOSE)) { inode = v9fs_get_inode_from_fid(v9ses, fid, dir->i_sb); - else + } else { inode = v9fs_get_new_inode_from_fid(v9ses, fid, dir->i_sb); + } /* * If we had a rename on the server and a parallel lookup * for the new name, then make sure we instantiate with diff --git a/fs/9p/vfs_super.c b/fs/9p/vfs_super.c index 431f24938a1d..94d6b02c221b 100644 --- a/fs/9p/vfs_super.c +++ b/fs/9p/vfs_super.c @@ -330,6 +330,7 @@ static int v9fs_init_fs_context(struct fs_context *fc) ctx->session_opts.uid = INVALID_UID; ctx->session_opts.dfltuid = V9FS_DEFUID; ctx->session_opts.dfltgid = V9FS_DEFGID; + ctx->session_opts.ndentry_timeout_ms = 0; /* initialize client options */ ctx->client_opts.proto_version = p9_proto_2000L; diff --git a/include/net/9p/client.h b/include/net/9p/client.h index 838a94218b59..55c6cb54bd25 100644 --- a/include/net/9p/client.h +++ b/include/net/9p/client.h @@ -192,6 +192,7 @@ struct p9_rdma_opts { * @dfltgid: default numeric groupid to mount hierarchy as * @uid: if %V9FS_ACCESS_SINGLE, the numeric uid which mounted the hierarchy * @session_lock_timeout: retry interval for blocking locks + * @ndentry_timeout_ms: Negative dentry lookup cache retention time in ms * * This strucure holds options which are parsed and will be transferred * to the v9fs_session_info structure when mounted, and therefore largely @@ -203,6 +204,7 @@ struct p9_session_opts { unsigned short debug; unsigned int afid; unsigned int cache; + unsigned int ndentry_timeout_ms; #ifdef CONFIG_9P_FSCACHE char *cachetag; #endif From 5670a84b5cda6b82016282accfd61ef36aceafbf Mon Sep 17 00:00:00 2001 From: Remi Pommarel Date: Thu, 21 May 2026 11:40:30 +0200 Subject: [PATCH 09/16] 9p: Add mount option for negative dentry cache retention Introduce a new mount option, negtimeout, for v9fs that allows users to specify how long negative dentries are retained in the cache. The retention time can be set in milliseconds (e.g. negtimeout=10000 for a 10secs retention time) or a negative value (e.g. negtimeout=-1) to keep negative entries until the buffer cache management removes them. For consistency reasons, this option should only be used in exclusive or read-only mount scenarios, aligning with the cache=loose usage. Signed-off-by: Remi Pommarel Message-ID: Signed-off-by: Dominique Martinet --- Documentation/filesystems/9p.rst | 5 +++++ fs/9p/v9fs.c | 16 +++++++++++++++- fs/9p/v9fs.h | 23 +++++++++++++---------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Documentation/filesystems/9p.rst b/Documentation/filesystems/9p.rst index 65809a1dad21..3f65db648db0 100644 --- a/Documentation/filesystems/9p.rst +++ b/Documentation/filesystems/9p.rst @@ -235,6 +235,11 @@ Options cachetag cache tag to use the specified persistent cache. cache tags for existing cache sessions can be listed at /sys/fs/9p/caches. (applies only to cache=fscache) + + negtimeout the duration (in milliseconds) that negative dentries (paths + that do not actually exist) are retained in the cache. If + set to a negative value, those entries are kept indefinitely + until evicted by the buffer cache management system ============= =============================================================== Behavior diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c index be83744b75b2..3e758b66fefa 100644 --- a/fs/9p/v9fs.c +++ b/fs/9p/v9fs.c @@ -39,7 +39,7 @@ enum { * source if we rejected it as EINVAL */ Opt_source, /* Options that take integer arguments */ - Opt_debug, Opt_dfltuid, Opt_dfltgid, Opt_afid, + Opt_debug, Opt_dfltuid, Opt_dfltgid, Opt_afid, Opt_negtimeout, /* String options */ Opt_uname, Opt_remotename, Opt_cache, Opt_cachetag, /* Options that take no arguments */ @@ -93,6 +93,7 @@ const struct fs_parameter_spec v9fs_param_spec[] = { fsparam_string ("access", Opt_access), fsparam_flag ("posixacl", Opt_posixacl), fsparam_u32 ("locktimeout", Opt_locktimeout), + fsparam_s32 ("negtimeout", Opt_negtimeout), /* client options */ fsparam_u32 ("msize", Opt_msize), @@ -159,6 +160,9 @@ int v9fs_show_options(struct seq_file *m, struct dentry *root) from_kgid_munged(&init_user_ns, v9ses->dfltgid)); if (v9ses->afid != ~0) seq_printf(m, ",afid=%u", v9ses->afid); + if (v9ses->flags & V9FS_NDENTRY_TIMEOUT_SET) + seq_printf(m, ",negtimeout=%d", + (int)v9ses->ndentry_timeout_ms); if (strcmp(v9ses->uname, V9FS_DEFUSER) != 0) seq_printf(m, ",uname=%s", v9ses->uname); if (strcmp(v9ses->aname, V9FS_DEFANAME) != 0) @@ -337,6 +341,16 @@ int v9fs_parse_param(struct fs_context *fc, struct fs_parameter *param) session_opts->session_lock_timeout = (long)result.uint_32 * HZ; break; + case Opt_negtimeout: + session_opts->flags |= V9FS_NDENTRY_TIMEOUT_SET; + if (result.int_32 < 0) { + session_opts->ndentry_timeout_ms = + NDENTRY_TIMEOUT_NEVER; + } else { + session_opts->ndentry_timeout_ms = result.int_32; + } + break; + /* Options for client */ case Opt_msize: if (result.uint_32 < 4096) { diff --git a/fs/9p/v9fs.h b/fs/9p/v9fs.h index e630c5111d74..a462bcbfc7da 100644 --- a/fs/9p/v9fs.h +++ b/fs/9p/v9fs.h @@ -24,6 +24,8 @@ * @V9FS_ACCESS_ANY: use a single attach for all users * @V9FS_ACCESS_MASK: bit mask of different ACCESS options * @V9FS_POSIX_ACL: POSIX ACLs are enforced + * @V9FS_NDENTRY_TIMEOUT_SET: Has negative dentry timeout retention time been + * overridden by negtimeout mount option * * Session flags reflect options selected by users at mount time */ @@ -34,16 +36,17 @@ #define V9FS_ACL_MASK V9FS_POSIX_ACL enum p9_session_flags { - V9FS_PROTO_2000U = 0x01, - V9FS_PROTO_2000L = 0x02, - V9FS_ACCESS_SINGLE = 0x04, - V9FS_ACCESS_USER = 0x08, - V9FS_ACCESS_CLIENT = 0x10, - V9FS_POSIX_ACL = 0x20, - V9FS_NO_XATTR = 0x40, - V9FS_IGNORE_QV = 0x80, /* ignore qid.version for cache hints */ - V9FS_DIRECT_IO = 0x100, - V9FS_SYNC = 0x200 + V9FS_PROTO_2000U = 0x01, + V9FS_PROTO_2000L = 0x02, + V9FS_ACCESS_SINGLE = 0x04, + V9FS_ACCESS_USER = 0x08, + V9FS_ACCESS_CLIENT = 0x10, + V9FS_POSIX_ACL = 0x20, + V9FS_NO_XATTR = 0x40, + V9FS_IGNORE_QV = 0x80, /* ignore qid.version for cache hints */ + V9FS_DIRECT_IO = 0x100, + V9FS_SYNC = 0x200, + V9FS_NDENTRY_TIMEOUT_SET = 0x400, }; /** From 15bbf82857fa61de9bad9faa7a3cf3a95b1b292a Mon Sep 17 00:00:00 2001 From: Remi Pommarel Date: Thu, 21 May 2026 11:40:31 +0200 Subject: [PATCH 10/16] 9p: Set default negative dentry retention time for cache=loose For cache=loose mounts, set the default negative dentry cache retention time to 24 hours. Signed-off-by: Remi Pommarel Message-ID: Signed-off-by: Dominique Martinet --- fs/9p/v9fs.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c index 3e758b66fefa..274c5157135d 100644 --- a/fs/9p/v9fs.c +++ b/fs/9p/v9fs.c @@ -24,6 +24,9 @@ #include "v9fs_vfs.h" #include "cache.h" +/* cache=loose default negative dentry retention time is 24hours */ +#define CACHE_LOOSE_NDENTRY_TIMEOUT_DEFAULT (24 * 60 * 60 * 1000) + static DEFINE_SPINLOCK(v9fs_sessionlist_lock); static LIST_HEAD(v9fs_sessionlist); struct kmem_cache *v9fs_inode_cache; @@ -441,6 +444,13 @@ static void v9fs_apply_options(struct v9fs_session_info *v9ses, v9ses->uid = ctx->session_opts.uid; v9ses->session_lock_timeout = ctx->session_opts.session_lock_timeout; v9ses->ndentry_timeout_ms = ctx->session_opts.ndentry_timeout_ms; + + /* If negative dentry timeout has not been overridden set default for + * cache=loose + */ + if (!(v9ses->flags & V9FS_NDENTRY_TIMEOUT_SET) && + (v9ses->cache & CACHE_LOOSE)) + v9ses->ndentry_timeout_ms = CACHE_LOOSE_NDENTRY_TIMEOUT_DEFAULT; } /** From 712da38d134b6681946368dd5c9400b43eb772b0 Mon Sep 17 00:00:00 2001 From: Remi Pommarel Date: Thu, 21 May 2026 11:40:32 +0200 Subject: [PATCH 11/16] 9p: Enable symlink caching in page cache Currently, when cache=loose is enabled, file reads are cached in the page cache, but symlink reads are not. This patch allows the results of p9_client_readlink() to be stored in the page cache, eliminating the need for repeated 9P transactions on subsequent symlink accesses. This change improves performance for workloads that involve frequent symlink resolution. Signed-off-by: Remi Pommarel Message-ID: <982462d17c0c0d2856763266a25eb04d080c1dbb.1779355927.git.repk@triplefau.lt> Signed-off-by: Dominique Martinet --- fs/9p/vfs_addr.c | 36 ++++++++++++++++++++++++++-- fs/9p/vfs_inode.c | 6 +++-- fs/9p/vfs_inode_dotl.c | 54 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/fs/9p/vfs_addr.c b/fs/9p/vfs_addr.c index 862164181bac..2b6ca573f955 100644 --- a/fs/9p/vfs_addr.c +++ b/fs/9p/vfs_addr.c @@ -70,11 +70,34 @@ static void v9fs_issue_read(struct netfs_io_subrequest *subreq) { struct netfs_io_request *rreq = subreq->rreq; struct p9_fid *fid = rreq->netfs_priv; + char *target; unsigned long long pos = subreq->start + subreq->transferred; - int total, err; + int total = 0, err, len, n; - total = p9_client_read(fid, pos, &subreq->io_iter, &err); + if (S_ISLNK(rreq->inode->i_mode)) { + /* p9_client_readlink() must not be called for legacy protocols + * 9p2000 or 9p2000.u. + */ + BUG_ON(!p9_is_proto_dotl(fid->clnt)); + if (WARN_ON_ONCE(pos)) { + /* reading a link at a non null offset should + * not happen + */ + err = -EIO; + goto fill_subreq; + } + err = p9_client_readlink(fid, &target); + if (err != 0) + goto fill_subreq; + len = strlen(target); + n = copy_to_iter(target, len, &subreq->io_iter); + kfree(target); + total = n; + } else { + total = p9_client_read(fid, pos, &subreq->io_iter, &err); + } +fill_subreq: /* if we just extended the file size, any portion not in * cache won't be on server and is zeroes */ if (subreq->rreq->origin != NETFS_UNBUFFERED_READ && @@ -99,6 +122,7 @@ static void v9fs_issue_read(struct netfs_io_subrequest *subreq) static int v9fs_init_request(struct netfs_io_request *rreq, struct file *file) { struct p9_fid *fid; + struct dentry *dentry; bool writing = (rreq->origin == NETFS_READ_FOR_WRITE || rreq->origin == NETFS_WRITETHROUGH || rreq->origin == NETFS_UNBUFFERED_WRITE || @@ -115,6 +139,14 @@ static int v9fs_init_request(struct netfs_io_request *rreq, struct file *file) if (!fid) goto no_fid; p9_fid_get(fid); + } else if (S_ISLNK(rreq->inode->i_mode)) { + dentry = d_find_any_alias(rreq->inode); + if (!dentry) + goto no_fid; + fid = v9fs_fid_lookup(dentry); + dput(dentry); + if (IS_ERR(fid)) + goto no_fid; } else { fid = v9fs_fid_find_inode(rreq->inode, writing, INVALID_UID, true); if (!fid) diff --git a/fs/9p/vfs_inode.c b/fs/9p/vfs_inode.c index a178e8cb2c82..cdaa5034cbef 100644 --- a/fs/9p/vfs_inode.c +++ b/fs/9p/vfs_inode.c @@ -302,10 +302,12 @@ int v9fs_init_inode(struct v9fs_session_info *v9ses, goto error; } - if (v9fs_proto_dotl(v9ses)) + if (v9fs_proto_dotl(v9ses)) { inode->i_op = &v9fs_symlink_inode_operations_dotl; - else + inode_nohighmem(inode); + } else { inode->i_op = &v9fs_symlink_inode_operations; + } break; case S_IFDIR: diff --git a/fs/9p/vfs_inode_dotl.c b/fs/9p/vfs_inode_dotl.c index fae324681ff3..8b056c334c2b 100644 --- a/fs/9p/vfs_inode_dotl.c +++ b/fs/9p/vfs_inode_dotl.c @@ -686,9 +686,11 @@ v9fs_vfs_symlink_dotl(struct mnt_idmap *idmap, struct inode *dir, int err; kgid_t gid; const unsigned char *name; + struct v9fs_session_info *v9ses; struct p9_qid qid; struct p9_fid *dfid; struct p9_fid *fid = NULL; + struct inode *inode; name = dentry->d_name.name; p9_debug(P9_DEBUG_VFS, "%lu,%s,%s\n", dir->i_ino, name, symname); @@ -712,6 +714,26 @@ v9fs_vfs_symlink_dotl(struct mnt_idmap *idmap, struct inode *dir, v9fs_invalidate_inode_attr(dir); + /* instantiate inode and assign the unopened fid to the dentry */ + fid = p9_client_walk(dfid, 1, &name, 1); + if (IS_ERR(fid)) { + err = PTR_ERR(fid); + p9_debug(P9_DEBUG_VFS, "p9_client_walk failed %d\n", + err); + goto error; + } + + v9ses = v9fs_inode2v9ses(dir); + inode = v9fs_get_new_inode_from_fid(v9ses, fid, dir->i_sb); + if (IS_ERR(inode)) { + err = PTR_ERR(inode); + p9_debug(P9_DEBUG_VFS, "inode creation failed %d\n", + err); + goto error; + } + v9fs_fid_add(dentry, &fid); + d_instantiate(dentry, inode); + err = 0; error: p9_fid_put(fid); p9_fid_put(dfid); @@ -853,16 +875,18 @@ v9fs_vfs_mknod_dotl(struct mnt_idmap *idmap, struct inode *dir, } /** - * v9fs_vfs_get_link_dotl - follow a symlink path + * v9fs_vfs_get_link_nocache_dotl - Resolve a symlink directly. + * + * To be used when symlink caching is not enabled. + * * @dentry: dentry for symlink * @inode: inode for symlink * @done: destructor for return value */ - static const char * -v9fs_vfs_get_link_dotl(struct dentry *dentry, - struct inode *inode, - struct delayed_call *done) +v9fs_vfs_get_link_nocache_dotl(struct dentry *dentry, + struct inode *inode, + struct delayed_call *done) { struct p9_fid *fid; char *target; @@ -884,6 +908,26 @@ v9fs_vfs_get_link_dotl(struct dentry *dentry, return target; } +/** + * v9fs_vfs_get_link_dotl - follow a symlink path + * @dentry: dentry for symlink + * @inode: inode for symlink + * @done: destructor for return value + */ +static const char * +v9fs_vfs_get_link_dotl(struct dentry *dentry, + struct inode *inode, + struct delayed_call *done) +{ + struct v9fs_session_info *v9ses; + + v9ses = v9fs_inode2v9ses(inode); + if (v9ses->cache & (CACHE_META|CACHE_LOOSE)) + return page_get_link(dentry, inode, done); + + return v9fs_vfs_get_link_nocache_dotl(dentry, inode, done); +} + int v9fs_refresh_inode_dotl(struct p9_fid *fid, struct inode *inode) { struct p9_stat_dotl *st; From 96a6db3fd7763f676fb6a25658b11aaf0e4fa4f9 Mon Sep 17 00:00:00 2001 From: Dominique Martinet Date: Fri, 29 May 2026 12:09:20 +0900 Subject: [PATCH 12/16] 9p: v9fs_file_do_lock: replace WARN_ONCE with p9_debug This warning depends on server-provided data, we should not use WARN here Reported-by: Yifei Chu Closes: https://lore.kernel.org/r/CAPJnbgJ7ZK7DCjCfG56hd_iKGePmAzudb4hOWd4=9r32nM+KcA@mail.gmail.com Signed-off-by: Dominique Martinet Message-ID: <20260529-lock-warn-v1-1-20c29580d61d@codewreck.org> --- fs/9p/vfs_file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/9p/vfs_file.c b/fs/9p/vfs_file.c index c5e73c37baea..cddfb4b7ce4e 100644 --- a/fs/9p/vfs_file.c +++ b/fs/9p/vfs_file.c @@ -198,7 +198,7 @@ static int v9fs_file_do_lock(struct file *filp, int cmd, struct file_lock *fl) res = -EAGAIN; break; default: - WARN_ONCE(1, "unknown lock status code: %d\n", status); + p9_debug(P9_DEBUG_ERROR, "unknown lock status code: %d\n", status); fallthrough; case P9_LOCK_ERROR: case P9_LOCK_GRACE: From 7d54894a1ee265a72d70f7cae1da6cc774cccc71 Mon Sep 17 00:00:00 2001 From: Yizhou Zhao Date: Fri, 29 May 2026 15:39:31 +0800 Subject: [PATCH 13/16] net/9p: fix race condition on rdma->state in trans_rdma.c The rdma->state field is modified without holding req_lock in both recv_done() and p9_cm_event_handler(), while rdma_request() accesses the same field under the req_lock spinlock. This inconsistent locking creates a race condition: - recv_done() running in softirq completion context sets rdma->state = P9_RDMA_FLUSHING without acquiring req_lock - p9_cm_event_handler() modifies rdma->state at multiple points (ADDR_RESOLVED, ROUTE_RESOLVED, ESTABLISHED, CLOSED) without req_lock - rdma_request() uses spin_lock_irqsave(&rdma->req_lock, flags) to protect the read-modify-write of rdma->state The race can cause lost state transitions: recv_done() or the CM event handler could set state to FLUSHING/CLOSED while rdma_request() is concurrently checking or modifying state under the lock, leading to the FLUSHING transition being silently overwritten by CLOSING. This corrupts the connection state machine and can cause use-after-free on RDMA request objects during teardown. Fix by adding req_lock protection to all rdma->state modifications in recv_done() and p9_cm_event_handler(), matching the pattern already used in rdma_request(). Use spin_lock_irqsave/spin_unlock_irqrestore in the CM event handler since it can race with recv_done() which runs in softirq context. Tested with a kernel module that races two threads (simulating rdma_request and recv_done/CM handler) on rdma->state with proper locking: 5.5M+ FLUSHING writes over 27M iterations with 0 lost transitions. Fixes: 473c7dd1d7b5 ("9p/rdma: remove useless check in cm_event_handler") Reported-by: Yizhou Zhao Reported-by: Yuxiang Yang Reported-by: Ao Wang Reported-by: Xuewei Feng Reported-by: Qi Li Reported-by: Ke Xu Assisted-by: GLM:GLM-5.1 Signed-off-by: Yizhou Zhao Message-ID: <20260529073933.77315-1-zhaoyz24@mails.tsinghua.edu.cn> Signed-off-by: Dominique Martinet --- net/9p/trans_rdma.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/net/9p/trans_rdma.c b/net/9p/trans_rdma.c index aa5bd74d333f..b4274f10fa44 100644 --- a/net/9p/trans_rdma.c +++ b/net/9p/trans_rdma.c @@ -128,25 +128,36 @@ p9_cm_event_handler(struct rdma_cm_id *id, struct rdma_cm_event *event) { struct p9_client *c = id->context; struct p9_trans_rdma *rdma = c->trans; + unsigned long flags; + switch (event->event) { case RDMA_CM_EVENT_ADDR_RESOLVED: + spin_lock_irqsave(&rdma->req_lock, flags); BUG_ON(rdma->state != P9_RDMA_INIT); rdma->state = P9_RDMA_ADDR_RESOLVED; + spin_unlock_irqrestore(&rdma->req_lock, flags); break; case RDMA_CM_EVENT_ROUTE_RESOLVED: + spin_lock_irqsave(&rdma->req_lock, flags); BUG_ON(rdma->state != P9_RDMA_ADDR_RESOLVED); rdma->state = P9_RDMA_ROUTE_RESOLVED; + spin_unlock_irqrestore(&rdma->req_lock, flags); break; case RDMA_CM_EVENT_ESTABLISHED: + spin_lock_irqsave(&rdma->req_lock, flags); BUG_ON(rdma->state != P9_RDMA_ROUTE_RESOLVED); rdma->state = P9_RDMA_CONNECTED; + spin_unlock_irqrestore(&rdma->req_lock, flags); break; case RDMA_CM_EVENT_DISCONNECTED: - if (rdma) + if (rdma) { + spin_lock_irqsave(&rdma->req_lock, flags); rdma->state = P9_RDMA_CLOSED; + spin_unlock_irqrestore(&rdma->req_lock, flags); + } c->status = Disconnected; break; @@ -184,6 +195,7 @@ recv_done(struct ib_cq *cq, struct ib_wc *wc) struct p9_req_t *req; int err = 0; int16_t tag; + unsigned long flags; req = NULL; ib_dma_unmap_single(rdma->cm_id->device, c->busa, client->msize, @@ -220,7 +232,10 @@ recv_done(struct ib_cq *cq, struct ib_wc *wc) err_out: p9_debug(P9_DEBUG_ERROR, "req %p err %d status %d\n", req, err, wc->status); - rdma->state = P9_RDMA_FLUSHING; + spin_lock_irqsave(&rdma->req_lock, flags); + if (rdma->state < P9_RDMA_FLUSHING) + rdma->state = P9_RDMA_FLUSHING; + spin_unlock_irqrestore(&rdma->req_lock, flags); client->status = Disconnected; goto out; } From 574aa0b4799470ac814479f1138d19efe6262255 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Tue, 21 Apr 2026 02:41:09 -0700 Subject: [PATCH 14/16] 9p: skip nlink update in cacheless mode to fix WARN_ON v9fs_dec_count() unconditionally calls drop_nlink() on regular files, even when the inode's nlink is already zero. In cacheless mode the client refetches inode metadata from the server (the source of truth) on every operation, so by the time v9fs_remove() returns, the locally cached nlink may already reflect the post-unlink value: 1. Client initiates unlink, server processes it and sets nlink to 0 2. Client refetches inode metadata (nlink=0) before unlink returns 3. Client's v9fs_remove() completes successfully 4. Client calls v9fs_dec_count() which calls drop_nlink() on nlink=0 This race is easily triggered under heavy unlink workloads, such as stress-ng's unlink stressor, producing the following warning: WARNING: fs/inode.c:417 at drop_nlink+0x4c/0xc8 Call trace: drop_nlink+0x4c/0xc8 v9fs_remove+0x1e0/0x250 [9p] v9fs_vfs_unlink+0x20/0x38 [9p] vfs_unlink+0x13c/0x258 ... In cacheless mode the server is authoritative and the inode is on its way out, so locally adjusting nlink buys nothing. Skip v9fs_dec_count() entirely when neither CACHE_META nor CACHE_LOOSE is set, which both avoids the warning and removes a class of nlink races (two concurrent unlinkers observing nlink > 0 and both calling drop_nlink()) that an nlink == 0 guard alone would only narrow rather than close. Fixes: ac89b2ef9b55 ("9p: don't maintain dir i_nlink if the exported fs doesn't either") Cc: stable@vger.kernel.org Suggested-by: Dominique Martinet Signed-off-by: Breno Leitao Message-ID: <20260421-9p-v2-1-48762d294fad@debian.org> Signed-off-by: Dominique Martinet --- fs/9p/vfs_inode.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/9p/vfs_inode.c b/fs/9p/vfs_inode.c index cdaa5034cbef..3a811db2dc19 100644 --- a/fs/9p/vfs_inode.c +++ b/fs/9p/vfs_inode.c @@ -490,10 +490,19 @@ static int v9fs_at_to_dotl_flags(int flags) * - ext4 (with dir_nlink feature enabled) sets nlink to 1 if a dir has more * than EXT4_LINK_MAX (65000) links. * + * In cacheless mode the server is the source of truth for nlink and the + * inode is going away immediately, so locally adjusting i_nlink buys + * nothing and races with concurrent metadata fetches that may already + * have observed the post-unlink value (nlink == 0). + * * @inode: inode whose nlink is being dropped */ static void v9fs_dec_count(struct inode *inode) { + struct v9fs_session_info *v9ses = v9fs_inode2v9ses(inode); + + if (!(v9ses->cache & (CACHE_META | CACHE_LOOSE))) + return; if (!S_ISDIR(inode->i_mode) || inode->i_nlink > 2) drop_nlink(inode); } From cc8b15a2c435bd1caf19741ba85286846a115764 Mon Sep 17 00:00:00 2001 From: David Laight Date: Sat, 6 Jun 2026 21:27:42 +0100 Subject: [PATCH 15/16] net/9p: Replace strlen() strcpy() pair with strscpy() Use the result of strscpy() for the overflow check. Signed-off-by: David Laight Message-ID: <20260606202744.5113-3-david.laight.linux@gmail.com> Signed-off-by: Dominique Martinet --- net/9p/trans_fd.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/net/9p/trans_fd.c b/net/9p/trans_fd.c index dbad3213ba84..eb685b52aeb2 100644 --- a/net/9p/trans_fd.c +++ b/net/9p/trans_fd.c @@ -940,14 +940,12 @@ p9_fd_create_unix(struct p9_client *client, struct fs_context *fc) if (!addr || !strlen(addr)) return -EINVAL; - if (strlen(addr) >= UNIX_PATH_MAX) { + sun_server.sun_family = PF_UNIX; + if (strscpy(sun_server.sun_path, addr) < 0) { pr_err("%s (%d): address too long: %s\n", __func__, task_pid_nr(current), addr); return -ENAMETOOLONG; } - - sun_server.sun_family = PF_UNIX; - strcpy(sun_server.sun_path, addr); err = __sock_create(current->nsproxy->net_ns, PF_UNIX, SOCK_STREAM, 0, &csocket, 1); if (err < 0) { From aa88278693cbfaf7a2acf961379973fbb63b165c Mon Sep 17 00:00:00 2001 From: Gui-Dong Han Date: Fri, 29 May 2026 15:54:41 +0800 Subject: [PATCH 16/16] 9p: Add missing read barrier in virtio zero-copy path Commit 2b6e72ed747f ("9P: Add memory barriers to protect request fields over cb/rpc threads handoff") added a read barrier after p9_client_rpc() waits for req->status, pairing with the write barrier in p9_client_cb(). The virtio zero-copy wait path was missed. Add the same read barrier after the zero-copy wait before reading the completed request. Fixes: 2b6e72ed747f ("9P: Add memory barriers to protect request fields over cb/rpc threads handoff") Signed-off-by: Gui-Dong Han Message-ID: <20260529075441.233369-1-hanguidong02@gmail.com> Signed-off-by: Dominique Martinet --- net/9p/trans_virtio.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/net/9p/trans_virtio.c b/net/9p/trans_virtio.c index 4cdab7094b27..b0d0094ec8e2 100644 --- a/net/9p/trans_virtio.c +++ b/net/9p/trans_virtio.c @@ -532,6 +532,11 @@ p9_virtio_zc_request(struct p9_client *client, struct p9_req_t *req, p9_debug(P9_DEBUG_TRANS, "virtio request kicked\n"); err = io_wait_event_killable(req->wq, READ_ONCE(req->status) >= REQ_STATUS_RCVD); + /* + * Make sure our req is coherent with regard to updates in other + * threads - echoes to wmb() in the callback + */ + smp_rmb(); // RERROR needs reply (== error string) in static data if (READ_ONCE(req->status) == REQ_STATUS_RCVD && unlikely(req->rc.sdata[4] == P9_RERROR))