All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Cai Xinchen" <caixinchen1@huawei.com>
Cc: bpf@vger.kernel.org, linux-security-module@vger.kernel.org,
	linux-trace-kernel@vger.kernel.org, selinux@vger.kernel.org,
	dsterba@suse.cz, "Chuck Lever" <cel@kernel.org>,
	"Jeff Layton" <jlayton@kernel.org>,
	"Anna Schumaker" <anna@kernel.org>
Subject: Re: [PATCH v3 -next 05/18] LSM: pass struct path and struct file to the inode_setattr hook
Date: Tue, 27 Oct 2026 12:48:51 +0000	[thread overview]
Message-ID: <sashiko-outbox-186584@kernel.org> (raw)
In-Reply-To: <20261027121151.1887650-6-caixinchen1@huawei.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] vfs: do_fchmodat(), do_fchownat(), and do_utimes_path() reject O_PATH fds
- [High] lsm: missing __nullable on file parameter of inode_setattr hook
- [High] fs: AT_EMPTY_PATH regressions in do_utimes_path(), do_fchmodat(), and do_fchownat()
- [Medium] lsm: untrusted path->dentry breaks bpf_lsm_inode_setattr() dentry kfuncs
--

commit 5205e882fa7eb32587d3ca77d8a0f86441ec4e69
Author: Cai Xinchen <caixinchen1@huawei.com>

LSM: pass struct path and struct file to the inode_setattr hook

This commit updates the inode_setattr LSM hook and its callers to pass a
struct path and an optional struct file alongside the mnt_idmap. It also
dispatches AT_EMPTY_PATH operations in do_fchmodat(), do_fchownat(), and
do_utimes_path() to the file descriptor-based helpers so the struct file can
be threaded through notify_change().

> diff --git a/fs/open.c b/fs/open.c
> index 294d0ea7488e6..882985d4008bf 100644
> --- a/fs/open.c
> +++ b/fs/open.c
[ ... ]
> @@ -724,12 +729,22 @@ static int do_fchmodat(int dfd, const char __user *filename, umode_t mode,
>  	if (unlikely(flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)))
>  		return -EINVAL;
>  
> +	CLASS(filename_maybe_null, name)(filename, flags);
> +
> +	if (!name && dfd >= 0) {
> +		CLASS(fd, f)(dfd);
> +
> +		if (fd_empty(f))
> +			return -EBADF;
> +
> +		return vfs_fchmod(fd_file(f), mode);
> +	}
[ ... ]
> @@ -844,14 +860,24 @@ int do_fchownat(int dfd, const char __user *filename, uid_t user, gid_t group,
>  	if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
>  		return -EINVAL;
>  
> +	CLASS(filename_maybe_null, name)(filename, flag);
> +
> +	if (!name && dfd >= 0) {
> +		CLASS(fd, f)(dfd);
> +
> +		if (fd_empty(f))
> +			return -EBADF;
> +
> +		return vfs_fchown(fd_file(f), user, group);
> +	}

[Severity: High]
Does using CLASS(fd, f)(dfd) here and in do_fchmodat() and do_utimes_path()
break AT_EMPTY_PATH on O_PATH file descriptors?

Previously, an empty path with AT_EMPTY_PATH went through
filename_lookup() -> path_lookupat() -> path_init(), which looks up nd->dfd
using CLASS(fd_raw, f)(nd->dfd) (just as vfs_fstat() does for vfs_fstatat()):

fs/namei.c:path_init() {
	...
		CLASS(fd_raw, f)(nd->dfd);
		struct dentry *dentry;

		if (fd_empty(f))
			return ERR_PTR(-EBADF);
	...
}

Supporting O_PATH file descriptors without /proc/self/fd was the reason
AT_EMPTY_PATH was added to fchmodat2(2) in commit 5daeb41a6fc9 ("fchmodat2:
add support for AT_EMPTY_PATH"), and is documented UAPI for fchownat(2) and
utimensat(2), since fchmod(2), fchown(2), and futimens(3) reject O_PATH
descriptors with EBADF.

By switching to CLASS(fd, f)(dfd), which calls fdget(dfd) ->
__fget_light(dfd, FMODE_PATH), fd_empty(f) is true whenever FMODE_PATH is
set, causing fchmodat2(dfd, "", mode, AT_EMPTY_PATH),
fchownat(dfd, "", user, group, AT_EMPTY_PATH), and
utimensat(dfd, "", times, AT_EMPTY_PATH) on an O_PATH fd to fail with -EBADF.

In addition, in do_dentry_open(), O_PATH files return before
security_file_open() is called:

fs/open.c:do_dentry_open() {
	...
	if (unlikely(f->f_flags & O_PATH)) {
		f->f_mode = FMODE_PATH | FMODE_OPENED;
		file_set_fsnotify_mode(f, FMODE_NONOTIFY);
		f->f_op = &empty_fops;
		return 0;
	}
	...
}

Because an O_PATH struct file was never authorized at open(2) time, should
these callers use CLASS(fd_raw, f)(dfd) and ensure that LSMs checking the
struct file in security_inode_setattr() do not treat an FMODE_PATH file as
carrying open-time permissions?

> diff --git a/fs/utimes.c b/fs/utimes.c
> index 5193d504d289f..6021c481e742f 100644
> --- a/fs/utimes.c
> +++ b/fs/utimes.c
[ ... ]
> @@ -88,15 +89,25 @@ static int do_utimes_path(int dfd, const char __user *filename,
>  	if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))
>  		return -EINVAL;
>  
> +	CLASS(filename_maybe_null, name)(filename, flags);
> +
> +	if (!name && dfd >= 0) {
> +		CLASS(fd, f)(dfd);
> +
> +		if (fd_empty(f))
> +			return -EBADF;
> +
> +		return vfs_utimes(&fd_file(f)->f_path, times, fd_file(f));
> +	}

[Severity: High]
Beyond the CLASS(fd, f)(dfd) issue with O_PATH file descriptors (which also
makes the FMODE_PATH check for hook_path_setattr in commit "landlock: Add
READ_METADATA and WRITE_METADATA access rights" unreachable), can switching
do_utimes_path() from CLASS(filename_uflags, name) to
CLASS(filename_maybe_null, name) cause utimensat(AT_FDCWD, NULL, times,
AT_EMPTY_PATH) to modify the current working directory instead of returning
-EFAULT?

In do_utimes(), a NULL filename with dfd != AT_FDCWD is routed to
do_utimes_fd() (which rejects non-zero flags like AT_EMPTY_PATH with -EINVAL),
whereas dfd == AT_FDCWD is routed to do_utimes_path():

fs/utimes.c:do_utimes() {
	if (filename == NULL && dfd != AT_FDCWD)
		return do_utimes_fd(dfd, times, flags);
	return do_utimes_path(dfd, filename, times, flags);
}

When filename == NULL, dfd == AT_FDCWD, and flags == AT_EMPTY_PATH,
CLASS(filename_uflags, name) previously returned ERR_PTR(-EFAULT). With
CLASS(filename_maybe_null, name), name is NULL, the (!name && dfd >= 0) check
is skipped because AT_FDCWD < 0, and filename_lookup(AT_FDCWD, NULL, ...)
treats NULL as an empty path and updates the current working directory.

Also, when (!name && dfd >= 0) is taken in do_utimes_path(), it bypasses
filename_lookup() (which called audit_inode()) and calls vfs_utimes(). Unlike
vfs_fchmod() and vfs_fchown(), vfs_utimes() does not call audit_file().
Should audit_file(fd_file(f)) be called before vfs_utimes() on the fd paths?

> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> index 539525648b303..ea6e0fbaa3b9c 100644
> --- a/include/linux/lsm_hook_defs.h
> +++ b/include/linux/lsm_hook_defs.h
> @@ -141,8 +141,16 @@ LSM_HOOK(int, 0, inode_follow_link, struct dentry *dentry, struct inode *inode,
>  	 bool rcu)
>  LSM_HOOK(int, 0, inode_permission, const struct mnt_idmap *idmap,
>  	 struct inode *inode, int mask)
> -LSM_HOOK(int, 0, inode_setattr, const struct mnt_idmap *idmap, struct dentry *dentry,
> -	 struct iattr *attr)
> +/*
> + * For the hooks that pass a struct path: the VFS only sanctions
> + * passing the path to VFS helpers taking a struct path (e.g.,
> + * notify_change(), the *xattr() helpers); it makes no guarantees
> + * with regard to path->mnt, whose layout and implementation details
> + * are subject to change.  Implementations must not rely on its
> + * properties.
> + */
> +LSM_HOOK(int, 0, inode_setattr, const struct mnt_idmap *idmap,
> +	 const struct path *path, struct iattr *attr, struct file *file)

[Severity: High]
Could omitting a __nullable annotation on the struct file *file parameter of
inode_setattr allow a BPF LSM program to trigger a kernel NULL pointer
dereference when notify_change() is called with file == NULL?

All path-based and internal callers of notify_change() (such as
chmod_common(), chown_common(), vfs_utimes(), do_truncate(), and
__remove_privs()) pass file == NULL, which do_notify_change() forwards to
security_inode_setattr(idmap, path, attr, NULL).

In btf_ctx_access(), arguments of trusted BPF LSM hooks are marked
PTR_TO_BTF_ID | PTR_TRUSTED, and PTR_MAYBE_NULL is only added if the parameter
name ends with __nullable:

kernel/bpf/btf.c:btf_ctx_access() {
	...
	info->reg_type = PTR_TO_BTF_ID;
	if (prog_args_trusted(prog))
		info->reg_type |= PTR_TRUSTED;

	if (btf_param_match_suffix(btf, &args[arg], "__nullable"))
		info->reg_type |= PTR_MAYBE_NULL;
	...
}

Without __nullable (either in lsm_hook_defs.h or via a strong override in
kernel/bpf/bpf_lsm_proto.c like bpf_lsm_mmap_file()), the BPF verifier treats
file as a non-NULL trusted pointer and permits passing it without a NULL
check to kfuncs such as bpf_get_file_xattr() or bpf_real_data_inode(), which
immediately dereference file->f_path.dentry.

[Severity: Medium]
Does replacing struct dentry *dentry with const struct path *path in
inode_setattr also break sleepable BPF LSM programs that pass the dentry to
bpf_get_dentry_xattr(), bpf_set_dentry_xattr(), or bpf_remove_dentry_xattr()?

bpf_lsm_inode_setattr is listed in d_inode_locked_hooks in
fs/bpf_fs_kfuncs.c specifically so sleepable BPF LSM programs attached to
inode_setattr can call those dentry xattr kfuncs:

fs/bpf_fs_kfuncs.c {
	BTF_SET_START(d_inode_locked_hooks)
	...
	BTF_ID(func, bpf_lsm_inode_setattr)
	...
	BTF_SET_END(d_inode_locked_hooks)
}

However, struct path is not listed in BTF_TYPE_SAFE_TRUSTED in
type_is_trusted() in kernel/bpf/verifier.c. When a BPF program walks
path->dentry, check_ptr_to_btf_access() marks the resulting dentry pointer as
PTR_UNTRUSTED, causing the BPF verifier to reject calls to the dentry xattr
kfuncs.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261027121151.1887650-1-caixinchen1@huawei.com?part=5

  reply	other threads:[~2026-10-27 12:48 UTC|newest]

Thread overview: 39+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 12:11 [PATCH v3 -next 00/18] landlock: Add READ_METADATA and WRITE_METADATA access rights Cai Xinchen
2026-10-27 12:11 ` [PATCH v3 -next 01/18] fs: pass struct path to notify_change() Cai Xinchen
2026-10-27 12:07   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 02/18] LSM: pass struct path to the inode_setsecctx hook Cai Xinchen
2026-10-27 12:09   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 03/18] fs: pass struct path to xattr helpers Cai Xinchen
2026-10-27 12:16   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 04/18] fs: pass struct path to POSIX ACL helpers Cai Xinchen
2026-10-27 12:20   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 05/18] LSM: pass struct path and struct file to the inode_setattr hook Cai Xinchen
2026-10-27 12:48   ` sashiko-bot [this message]
2026-10-27 12:11 ` [PATCH v3 -next 06/18] LSM: pass struct path and struct file to the inode xattr hooks Cai Xinchen
2026-10-27 13:02   ` sashiko-bot
2026-10-27 15:01     ` Alexei Starovoitov
2026-10-28 10:51       ` Cai Xinchen
2026-10-27 12:11 ` [PATCH v3 -next 07/18] LSM: pass struct path and struct file to the inode posix acl hooks Cai Xinchen
2026-10-27 13:10   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 08/18] LSM: pass struct path and struct file to the inode fileattr hooks Cai Xinchen
2026-10-27 13:45   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 09/18] LSM: rename inode_getattr to path_getattr and pass struct file Cai Xinchen
2026-10-27 14:01   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 10/18] LSM: rename inode_setattr to path_setattr Cai Xinchen
2026-10-27 14:04   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 11/18] LSM: rename the inode xattr hooks to path_xattr hooks Cai Xinchen
2026-10-27 14:06   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 12/18] LSM: rename the inode posix acl hooks to path_*_acl hooks Cai Xinchen
2026-10-27 14:09   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 13/18] LSM: rename the inode fileattr hooks to path_file_* hooks Cai Xinchen
2026-10-27 14:12   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 14/18] LSM: rename inode_setsecctx to path_setsecctx Cai Xinchen
2026-10-27 14:14   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 15/18] landlock: Add READ_METADATA and WRITE_METADATA access rights Cai Xinchen
2026-10-27 14:49   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 16/18] selftests/landlock: Add tests for metadata " Cai Xinchen
2026-10-27 15:11   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 17/18] samples/landlock: Add metadata rights to sandboxer Cai Xinchen
2026-10-27 15:14   ` sashiko-bot
2026-10-27 12:11 ` [PATCH v3 -next 18/18] Documentation: Update landlock doc for metadata rights Cai Xinchen
2026-10-27 15:16   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-186584@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=anna@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=caixinchen1@huawei.com \
    --cc=cel@kernel.org \
    --cc=dsterba@suse.cz \
    --cc=jlayton@kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=linux-trace-kernel@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=selinux@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.