Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Documentation/filesystems/9p.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,48 @@ 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)

inodeident this setting controls how inodes work on this filesystem.
More specifically, how they are "reused". This is most
relevant when used with features like Landlock and
fanotify (in inode mark mode). These features rely on
holding a specific inode and identifying further access to
the same file (as identified by that inode).

There are 2 possible values:
qid
This is the default and the only possible
option if loose or metadata cache is
enabled. In this mode, 9pfs assumes that
the server will not present different
files with the same inode number, and will
use the presented inode number to lookup
inodes. For QEMU users, this can be
ensured by setting multidevs=remap. If
the server does present inode number
collisions, this may lead to unpredictable
behaviour when both files are accessed.
path
This is the default if neither loose nor
metadata cache bits are enabled. This
option causes 9pfs to internally track the
file path that an inode originated from,
and will only use an existing inode
(instead of allocating a new one) if the
path matches, even if the file's inode
number matches that of an existing inode.

.. note::
For inodeident=path, when a directory is renamed
or moved, inodeident=path mode currently does not
update its children's inodes to point to the new
path, and thus further access to them via the new
location will use newly allocated inodes, and
existing inode marks placed by Landlock and
fanotify on them will no longer work.

The inode path for the target being renamed itself
(not its children) *is* updated, however.
============= ===============================================================

Behavior
Expand Down
3 changes: 2 additions & 1 deletion fs/9p/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ obj-$(CONFIG_9P_FS) := 9p.o
vfs_dentry.o \
v9fs.o \
fid.o \
xattr.o
xattr.o \
ino_path.o

9p-$(CONFIG_9P_FSCACHE) += cache.o
9p-$(CONFIG_9P_FS_POSIX_ACL) += acl.o
111 changes: 111 additions & 0 deletions fs/9p/ino_path.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Specific operations on the v9fs_ino_path structure.
*
* Copyright (C) 2025 by Tingmao Wang <m@maowtm.org>
*/

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/fs.h>
#include <linux/string.h>
#include <linux/dcache.h>

#include <linux/posix_acl.h>
#include <net/9p/9p.h>
#include <net/9p/client.h>
#include "v9fs.h"

/*
* Must hold rename_sem due to traversing parents. Caller must hold
* reference to dentry.
*/
struct v9fs_ino_path *make_ino_path(struct dentry *dentry)
{
struct v9fs_ino_path *path;
size_t path_components = 0;
struct dentry *curr = dentry;
ssize_t i;

/* Either read or write lock held is ok */
lockdep_assert_held(&v9fs_dentry2v9ses(dentry)->rename_sem);
might_sleep(); /* Allocation below might block */

rcu_read_lock();

/* Don't include the root dentry */
while (curr->d_parent != curr) {
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison should use SIZE_MAX instead of SSIZE_MAX since path_components is of type size_t (unsigned). SSIZE_MAX is for signed types and may cause unexpected behavior.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= SIZE_MAX)) {

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison should use SIZE_MAX instead of SSIZE_MAX since path_components is of type size_t (unsigned). Using SSIZE_MAX could allow overflow in the unsigned arithmetic.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= SIZE_MAX)) {

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks against SSIZE_MAX but path_components is of type size_t (unsigned). This comparison will never be true since size_t cannot exceed SSIZE_MAX on most platforms, and the check should use SIZE_MAX instead.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= SIZE_MAX)) {

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using SSIZE_MAX as the limit for path_components (which is size_t) creates a type mismatch. Since path_components is unsigned and SSIZE_MAX is the maximum positive value for signed integers, this comparison may not work as intended. Consider using SIZE_MAX / 2 or defining an appropriate maximum path depth constant.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= V9FS_MAX_PATH_COMPONENTS)) {

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison should use SIZE_MAX instead of SSIZE_MAX since path_components is of type size_t (unsigned). Using SSIZE_MAX could allow overflow in the signed conversion.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= SIZE_MAX)) {

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using SSIZE_MAX as the limit for path_components is incorrect since path_components is declared as size_t (unsigned). This comparison will never be true on most systems. Consider using SIZE_MAX or a more reasonable path depth limit.

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using SSIZE_MAX as the limit is incorrect since path_components is declared as size_t (unsigned) but compared against a signed maximum. This should be SIZE_MAX or the variable should be ssize_t.

Suggested change
if (WARN_ON_ONCE(path_components >= SSIZE_MAX)) {
if (WARN_ON_ONCE(path_components >= SIZE_MAX)) {

Copilot uses AI. Check for mistakes.

rcu_read_unlock();
return NULL;
}
path_components++;
curr = curr->d_parent;
}

/*
* Allocation can block so don't do it in RCU (and because the
* allocation might be large, since name_snapshot leaves space for
* inline str, not worth trying GFP_ATOMIC)
*/
rcu_read_unlock();

path = kmalloc(struct_size(path, names, path_components), GFP_KERNEL);
if (!path)
return NULL;

path->nr_components = path_components;
curr = dentry;

rcu_read_lock();
for (i = path_components - 1; i >= 0; i--) {
take_dentry_name_snapshot(&path->names[i], curr);
curr = curr->d_parent;
}
WARN_ON(curr != curr->d_parent);
rcu_read_unlock();
return path;
}

void free_ino_path(struct v9fs_ino_path *path)
{
if (path) {
for (size_t i = 0; i < path->nr_components; i++)
release_dentry_name_snapshot(&path->names[i]);
kfree(path);
}
}

/*
* Must hold rename_sem due to traversing parents. Returns whether
* ino_path matches with the path of a v9fs dentry. This function does
* not sleep.
*/
bool ino_path_compare(struct v9fs_ino_path *ino_path, struct dentry *dentry)
{
struct dentry *curr = dentry;
struct name_snapshot *compare;
ssize_t i;
bool ret;

lockdep_assert_held_read(&v9fs_dentry2v9ses(dentry)->rename_sem);

rcu_read_lock();
for (i = ino_path->nr_components - 1; i >= 0; i--) {
Comment on lines +88 to +94
Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop uses a signed integer i (ssize_t) to iterate backwards from an unsigned value (size_t nr_components). When nr_components is 0, the loop will underflow and continue indefinitely since i starts at SIZE_MAX-1 and never becomes negative.

Suggested change
ssize_t i;
bool ret;
lockdep_assert_held_read(&v9fs_dentry2v9ses(dentry)->rename_sem);
rcu_read_lock();
for (i = ino_path->nr_components - 1; i >= 0; i--) {
size_t i;
bool ret;
lockdep_assert_held_read(&v9fs_dentry2v9ses(dentry)->rename_sem);
rcu_read_lock();
for (i = ino_path->nr_components; i-- > 0; ) {

Copilot uses AI. Check for mistakes.

if (curr->d_parent == curr) {
/* We're supposed to have more components to walk */
rcu_read_unlock();
return false;
}
compare = &ino_path->names[i];
if (!d_same_name(curr, curr->d_parent, &compare->name)) {
Copy link

Copilot AI Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The d_same_name function expects a parent dentry as the second parameter, but here curr->d_parent is passed while curr is the child. The parameter order should be verified against the function signature to ensure correct usage.

Copilot uses AI. Check for mistakes.

rcu_read_unlock();
return false;
}
curr = curr->d_parent;
}
/* Comparison fails if dentry is deeper than ino_path */
ret = (curr == curr->d_parent);
rcu_read_unlock();
return ret;
}
59 changes: 58 additions & 1 deletion fs/9p/v9fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ enum {
/* Options that take integer arguments */
Opt_debug, Opt_dfltuid, Opt_dfltgid, Opt_afid,
/* String options */
Opt_uname, Opt_remotename, Opt_cache, Opt_cachetag,
Opt_uname, Opt_remotename, Opt_cache, Opt_cachetag, Opt_inodeident,
/* Options that take no arguments */
Opt_nodevmap, Opt_noxattr, Opt_directio, Opt_ignoreqv,
/* Access options */
Expand All @@ -63,6 +63,7 @@ static const match_table_t tokens = {
{Opt_access, "access=%s"},
{Opt_posixacl, "posixacl"},
{Opt_locktimeout, "locktimeout=%u"},
{Opt_inodeident, "inodeident=%s"},
{Opt_err, NULL}
};

Expand Down Expand Up @@ -149,6 +150,21 @@ int v9fs_show_options(struct seq_file *m, struct dentry *root)
if (v9ses->flags & V9FS_NO_XATTR)
seq_puts(m, ",noxattr");

switch (v9ses->flags & V9FS_INODE_IDENT_MASK) {
case V9FS_INODE_IDENT_QID:
seq_puts(m, ",inodeident=qid");
break;
case V9FS_INODE_IDENT_PATH:
seq_puts(m, ",inodeident=path");
break;
default:
/*
* Unspecified, will be set later in v9fs_session_init depending on
* cache setting
*/
break;
}

return p9_show_client_options(m, v9ses->clnt);
}

Expand Down Expand Up @@ -369,6 +385,26 @@ static int v9fs_parse_options(struct v9fs_session_info *v9ses, char *opts)
v9ses->session_lock_timeout = (long)option * HZ;
break;

case Opt_inodeident:
s = match_strdup(&args[0]);
if (!s) {
ret = -ENOMEM;
p9_debug(P9_DEBUG_ERROR,
"problem allocating copy of inodeident arg\n");
goto free_and_return;
}
v9ses->flags &= ~V9FS_INODE_IDENT_MASK;
if (strcmp(s, "qid") == 0) {
v9ses->flags |= V9FS_INODE_IDENT_QID;
} else if (strcmp(s, "path") == 0) {
v9ses->flags |= V9FS_INODE_IDENT_PATH;
} else {
ret = -EINVAL;
p9_debug(P9_DEBUG_ERROR, "Unknown inodeident argument %s\n", s);
}
kfree(s);
break;
Comment on lines +404 to +406
Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling should use goto free_and_return after setting ret = -EINVAL to follow the established pattern in this function and ensure consistent error handling flow.

Suggested change
}
kfree(s);
break;
kfree(s);
goto free_and_return;
}

Copilot uses AI. Check for mistakes.


default:
continue;
}
Expand All @@ -393,6 +429,7 @@ struct p9_fid *v9fs_session_init(struct v9fs_session_info *v9ses,
{
struct p9_fid *fid;
int rc = -ENOMEM;
bool cached;

v9ses->uname = kstrdup(V9FS_DEFUSER, GFP_KERNEL);
if (!v9ses->uname)
Expand Down Expand Up @@ -427,6 +464,26 @@ struct p9_fid *v9fs_session_init(struct v9fs_session_info *v9ses,
if (rc < 0)
goto err_clnt;

cached = v9ses->cache & (CACHE_META | CACHE_LOOSE);

if (cached && v9ses->flags & V9FS_INODE_IDENT_PATH) {
rc = -EINVAL;
p9_debug(P9_DEBUG_ERROR,
"inodeident=path not supported in cached mode\n");
goto err_clnt;
}

if (!(v9ses->flags & V9FS_INODE_IDENT_MASK)) {
/* Unspecified - use default */
if (cached) {
/* which is qid in cached mode (path not supported) */
v9ses->flags |= V9FS_INODE_IDENT_QID;
} else {
/* ...or path in uncached mode */
v9ses->flags |= V9FS_INODE_IDENT_PATH;
}
}

v9ses->maxdata = v9ses->clnt->msize - P9_IOHDRSZ;

if (!v9fs_proto_dotl(v9ses) &&
Expand Down
Loading