vfs, afs, bash: Fix miscomparison of foreign user IDs in the VFS
Subsystems (10)
David Howells
maintainerv4 cover vfs, afs, bash: Fix miscomparison of foreign user IDs in the VFS
Hi Al, Christian,
Here's a pair of fixes that deal with some places the VFS mishandles foreign user ID checks. By "foreign" I mean that the user IDs from the filesystem do not belong in the same number space as the system's user IDs. Network filesystems are prime examples of this, but it may also impact things like USB drives or cdroms.
Take AFS as example: Whilst each file does have a numeric user ID, the file may be accessed from a world-accessible public-facing server from some other organisation with its own idea of what that user ID refers to. IDs from AFS may also collide with the system's own set of IDs and may also be unrepresentable as a 32-bit UID (in the case of AuriStor servers).
Further, kAFS uses a key containing an authentication token to specify the subject doing an RPC operation to the server - and, as such, this needs to be used instead of current_fsuid() in determining whether the current user has ownership rights over a file.
Additionally, filesystems (CIFS being a notable example) may also have user identifiers that aren't simple integers.
Now the problem in the VFS is that there are a number of places where it assumes it can directly compare i_uid (possibly id-mapped) to either than on another inode or a UID drawn from elsewhere (e.g. current_uid()) - but this doesn't work right.
This causes the write-to-sticky check to work incorrectly for AFS (though this is currently masked by a workaround in bash that has now been removed as of bash-5.3) whereby open(O_CREAT) of such a file will fail when it shouldn't.
Two patches are provided:
(1) Add a pair of inode operations, one to compare the ownership of a pair of inodes and the other to see if the current process has ownership rights over an inode with an IOP flag to control access to them.
Usage of this is then extended out into the VFS, replacing comparisons between i_uid and i_uid and between i_uid and current_fsuid(). The default, it the inode ops are unimplemented, is to do those direct i_uid comparisons.
(2) Fixes the bash workaround issue with regard to AFS, overriding the checks as to whether two inodes have the same owner and the check as to whether the current user owns an inode to work within the AFS model.
kAFS uses the result of a status-fetch with a suitable key to determine file ownership (if the ADMINISTER bit is set) and just compares the 64-bit owner IDs to determine if two inodes have the same ownership.
Note that chown may also need modifying in some way - but the system call as it stands can't necessarily supply the information required (for instance, an AuriStor YFS ID is 64 bits, but chown can only handle a 32-bit integer; CIFS might use a GUID).
The patches can be found here:
https://git.kernel.org/pub/scm/linux/kernel/git/dhowells/linux-fs.git/log/?h=afs-sticky-2
Thanks,
David
Changes
=======
ver #4)
- Rebased on v7.3-rc1.
ver #3)
- Fixed afs patch to provide ops for afs symlinks too.
ver #2)
- Rebased on v7.2-rc4.
- Added an IOP_ flag for a faster check of whether the new inode ops are supported.
David Howells (2):
vfs: Allow filesystems with foreign owner IDs to override UID checks afs, bash: Fix open(O_CREAT) on an extant AFS file in a sticky dir
Documentation/filesystems/vfs.rst | 21 ++++ fs/afs/dir.c | 2 + fs/afs/file.c | 2 + fs/afs/inode.c | 1 + fs/afs/internal.h | 3 + fs/afs/security.c | 46 +++++++++ fs/afs/symlink.c | 2 + fs/attr.c | 58 ++++++----- fs/coredump.c | 2 +- fs/inode.c | 11 +- fs/internal.h | 1 + fs/locks.c | 7 +- fs/namei.c | 161 ++++++++++++++++++++++++------ fs/remap_range.c | 20 ++-- include/linux/fs.h | 7 +- 15 files changed, 273 insertions(+), 71 deletions(-)
David Howells
maintainerPATCH v4 1/2 vfs: Allow filesystems with foreign owner IDs to override UID checks
A number of ownership checks made by the VFS make a number of assumptions:
(1) that it is meaningful to compare inode->i_uid to a second ->i_uid or to current_fsuid(),
(2) that current_fsuid() represents the subject of the action,
(3) that the number in ->i_uid belong to the system's ID space and
(4) that the IDs can be represented by 32-bit integers.
Network filesystems, however, may violate all four of these assumptions. Indeed, a network filesystem may not even have an actual concept of a UNIX integer UID (cifs without POSIX extensions, for example). Plug-in block filesystems (e.g. USB drives) may also violate this assumption.
In particular, AFS implements its own ACL security model with its own per-cell user ID space with 64-bit IDs for some server variants. The subject is represented by a token in a key, not current_fsuid(). The AFS user IDs and the system user IDs for a cell may be numerically equivalent, but that's matter of administrative policy and should perhaps be noted in the cell definition or by mount option. A subsequent patch will address AFS.
To help fix this, three functions are defined to perform UID comparison within the VFS:
(1) vfs_inode_is_owned_by_me(). This defaults to comparing i_uid to current_fsuid(), with appropriate namespace mapping, assuming that the fsuid identifies the subject of the action. The filesystem may override it by implementing an inode op:
int (*is_owned_by_me)(struct mnt_idmap *idmap, struct inode *inode);
This should return 0 if owned, 1 if not or an error if there's some sort of lookup failure. It may use a means of identifying the subject of the action other than fsuid, for example by using an authentication token stored in a key.
(2) vfs_inodes_have_same_owner(). This defaults to comparing the i_uids of two different inodes with appropriate namespace mapping. The filesystem may override it by implementing another inode op:
int (*have_same_owner)(struct mnt_idmap *idmap, struct inode *inode1,
struct inode *inode2);
Again, this should return 0 if matching, 1 if not or an error if there's some sort of lookup failure.
(3) vfs_inode_and_dir_have_same_owner(). This is similar to (2), but assumes that the second inode is the parent directory to the first and takes a nameidata struct instead of a second inode pointer.
In addition IOP_OWNERSHIP_OVERRIDE is added to i_opflags to control access to these new inode ops.
Fix a number of places within the VFS where such UID checks are made that should be deferring interpretation to the filesystem.
(*) chown_ok()
(*) chgrp_ok()
Call vfs_inode_is_owned_by_me(). Possibly these need to defer all their checks to the network filesystem as the interpretation of the new UID/GID depends on the netfs too, but the ->setattr() method gets a chance to deal with that.
(*) coredump_file()
Call vfs_is_owned_by_me() to check that the file created is owned by the caller - but the check that's there might be sufficient.
(*) inode_owner_or_capable()
Call vfs_is_owned_by_me(). I'm not sure whether the namespace mapping makes sense in such a case, but it probably could be used.
(*) vfs_setlease()
Call vfs_is_owned_by_me(). Actually, it should query if leasing is permitted.
Also, setting locks could perhaps do with a permission call to the filesystem driver as AFS, for example, has a lock permission bit in the ACL, but since the AFS server checks that when the RPC call is made, it's probably unnecessary.
(*) acl_permission_check()
(*) posix_acl_permission()
Unchanged. These functions are only used by generic_permission() which is overridden if ->permission() is supplied, and when evaluating a POSIX ACL, it should arguably be checking the UID anyway.
AFS, for example, implements its own ACLs and evaluates them in ->permission() and on the server.
(*) may_follow_link()
Call vfs_inode_and_dir_have_same_owner() and vfs_is_owned_by_me() on the the link and its parent dir.
(*) may_create_in_sticky()
Call vfs_is_owned_by_me() and also vfs_inode_and_dir_have_same_owner() both.
[?] Should this return ok immediately if the open call we're in created the file being checked.
(*) __check_sticky()
Call vfs_is_owned_by_me() on both the dir and the inode, but for AFS vfs_is_owned_by_me() on a directory doesn't work, so call vfs_inodes_have_same_owner() instead to check the directory (as is done in may_create_in_sticky()).
(*) may_dedupe_file()
Call vfs_is_owned_by_me().
(*) IMA policy ops.
Unchanged for now. I'm not sure what the best way to deal with this is - if, indeed, it needs any changes.
Note that wrapping stuff up into vfs_inode_is_owned_by_me() isn't necessarily the most efficient as it means we may end up doing the uid idmapping an extra time - though this is only done in three places, all to do with world-writable sticky dir checks.
--- Documentation/filesystems/vfs.rst | 21 ++++ fs/attr.c | 58 ++++++----- fs/coredump.c | 2 +- fs/inode.c | 11 +- fs/internal.h | 1 + fs/locks.c | 7 +- fs/namei.c | 161 ++++++++++++++++++++++++------ fs/remap_range.c | 20 ++-- include/linux/fs.h | 7 +- 9 files changed, 217 insertions(+), 71 deletions(-)
Documentation/filesystems/vfs.rst +21 -0
@@ -445,6 +445,9 @@ As of kernel 2.6.22, the following members are defined: struct dentry *dentry, struct file_kattr *fa); int (*fileattr_get)(struct dentry *dentry, struct file_kattr *fa); struct offset_ctx *(*get_offset_ctx)(struct inode *inode); + int (*is_owned_by_me)(struct mnt_idmap *idmap, struct inode *inode); + int (*have_same_owner)(struct mnt_idmap *idmap, struct inode *inode, + struct dentry *dentry); }; Again, all methods are called without any locks being held, unless
@@ -634,6 +637,24 @@ otherwise noted. filesystem must define this operation to use simple_offset_dir_operations. +``is_owned_by_me`` + called to determine if the file can be considered to be 'owned' by + the owner of the process or if the process has a token that grants + it ownership privileges. If unset, the default is to compare i_uid + to current_fsuid() - but this may give incorrect results for some + network or plug-in block filesystems. For example, AFS determines + ownership entirely according to an obtained token and i_uid may not + even be from the same ID space as current_uid(). + +``have_same_owner`` + called to determine if an inode has the same owner as its immediate + parent on the path walked. If unset, the default is to simply + compare the i_uid of both. For example, AFS compares the owner IDs + of both - but these are a 64-bit values on some variants that might + not fit into a kuid_t and cifs has GUIDs that cannot be compared to + kuid_t. + + The Address Space Object ========================
fs/attr.c +35 -23
103 lines not on this page · raw message
fs/coredump.c +1 -1
@@ -949,7 +949,7 @@ static bool coredump_file(struct core_name *cn, struct coredump_params *cprm,* filesystem. */ idmap =file_mnt_idmap (file); -if (!vfsuid_eq_kuid (i_uid_into_vfsuid (idmap, inode),current_fsuid ())) { +if (vfs_inode_is_owned_by_me (idmap, inode) !=0 ) {coredump_report_failure ("Core dump to %s aborted: cannot preserve file owner" , cn->corename);return false; }
fs/inode.c +7 -4
@@ -2746,16 +2746,19 @@ EXPORT_SYMBOL(inode_init_owner);* On non-idmapped mounts or if permission checking is to be performed on the * raw inode simply pass @nop_mnt_idmap. */ -bool inode_owner_or_capable (struct mnt_idmap *idmap, -const struct inode *inode) +bool inode_owner_or_capable (struct mnt_idmap *idmap,struct inode *inode) {vfsuid_t vfsuid;struct user_namespace *ns; +int ret; - vfsuid =i_uid_into_vfsuid (idmap, inode); -if (vfsuid_eq_kuid (vfsuid,current_fsuid ())) + ret =vfs_inode_is_owned_by_me (idmap, inode); +if (ret ==0 )return true; +if (ret <0 ) +return false; + vfsuid =i_uid_into_vfsuid (idmap, inode); ns =current_user_ns ();if (vfsuid_has_mapping (ns, vfsuid) &&ns_capable (ns,CAP_FOWNER ))return true;
fs/internal.h +1 -0
@@ -51,6 +51,7 @@ extern int finish_clean_context(struct fs_context *fc);/* * namei.c */ +int vfs_inode_is_owned_by_me (struct mnt_idmap *idmap,struct inode *inode);extern int filename_lookup (int dfd,struct filename *name,unsigned flags,struct path *path,const struct path *root);int filename_rmdir (int dfd,struct filename *name);
fs/locks.c +5 -2
@@ -68,6 +68,7 @@#include <trace/events/filelock.h> #include <linux/uaccess.h> +#include "internal.h" static struct file_lock *file_lock (struct file_lock_core *flc) {
@@ -2136,10 +2137,12 @@ int vfs_setlease(struct file *filp, int arg, struct file_lease **lease, void **priv) { struct inode *inode = file_inode(filp); -vfsuid_t vfsuid =i_uid_into_vfsuid (file_mnt_idmap (filp), inode);int error; -if ((!vfsuid_eq_kuid (vfsuid,current_fsuid ())) && !capable (CAP_LEASE )) + error =vfs_inode_is_owned_by_me (file_mnt_idmap (filp), inode); +if (error <0 ) +return error; +if (error !=0 && !capable (CAP_LEASE ))return -EACCES ; error =security_file_lock (filp, arg);if (error)
fs/namei.c +131 -30
222 lines not on this page · raw message
fs/remap_range.c +11 -9
@@ -413,20 +413,22 @@ loff_t vfs_clone_file_range(struct file *file_in, loff_t pos_in,EXPORT_SYMBOL (vfs_clone_file_range);/* Check whether we are allowed to dedupe the destination file */ -static bool may_dedupe_file (struct file *file) +static int may_dedupe_file (struct file *file) {struct mnt_idmap *idmap =file_mnt_idmap (file);struct inode *inode =file_inode (file); +int ret;if (capable (CAP_SYS_ADMIN )) -return true; +return 0 ;if (file->f_mode &FMODE_WRITE ) -return true; -if (vfsuid_eq_kuid (i_uid_into_vfsuid (idmap, inode),current_fsuid ())) -return true; +return 0 ; + ret =vfs_inode_is_owned_by_me (idmap, inode); +if (ret <=0 ) +return ret;if (!inode_permission (idmap, inode,MAY_WRITE )) -return true; -return false; +return 0 ; +return -EPERM ; } loff_t vfs_dedupe_file_range_one(struct file *src_file, loff_t src_pos,
@@ -459,8 +461,8 @@ loff_t vfs_dedupe_file_range_one(struct file *src_file, loff_t src_pos,if (ret)return ret; - ret = -EPERM ; -if (!may_dedupe_file (dst_file)) + ret =may_dedupe_file (dst_file); +if (ret <0 ) goto out_drop_write; ret = -EXDEV ;
include/linux/fs.h +5 -2
@@ -627,6 +627,7 @@ is_uncached_acl(struct posix_acl *acl)#define IOP_CACHED_LINK 0x0040#define IOP_FASTPERM_MAY_EXEC 0x0080#define IOP_FLCTX 0x0100 +#define IOP_OWNERSHIP_OVERRIDE 0x0200/* Use ->is_owned_by_me() and ->have_same_owner() */ /* * Inode state bits. Protected by inode->i_lock
@@ -1755,8 +1756,7 @@ static inline bool file_write_not_started(const struct file *file)return sb_write_not_started (file_inode (file)->i_sb); } -bool inode_owner_or_capable (struct mnt_idmap *idmap, -const struct inode *inode); +bool inode_owner_or_capable (struct mnt_idmap *idmap,struct inode *inode);/* * VFS helper functions..
@@ -2034,6 +2034,9 @@ struct inode_operations { struct dentry *dentry, struct file_kattr *fa);int (*fileattr_get)(struct dentry *dentry,struct file_kattr *fa);struct offset_ctx *(*get_offset_ctx)(struct inode *inode); +int (*is_owned_by_me)(struct mnt_idmap *idmap,struct inode *inode); +int (*have_same_owner)(struct mnt_idmap *idmap,struct inode *inode1, +struct inode *inode2); }____cacheline_aligned ;/* Did the driver provide valid mmap hook configuration? */
David Howells
maintainerPATCH v4 2/2 afs, bash: Fix open(O_CREAT) on an extant AFS file in a sticky dir
In reply to David HowellsSince version 1.11 (January 1992) Bash has a work around in redir_open() that causes open(O_CREAT) of a file to be retried without O_CREAT if open() fails with an EACCES error if bash was built with AFS workarounds configured:
#if defined (AFS)
if ((fd < 0) && (errno == EACCES))
{
fd = open (filename, flags & ~O_CREAT, mode);
errno = EACCES; /* restore errno */
}
#endif /* AFS */The ~O_CREAT fallback logic was introduced to workaround a bug[1] in the IBM AFS 3.1 cache manager and server which can return EACCES in preference to EEXIST if the requested file exists but the caller is neither granted explicit PRSFS_READ permission nor is the file owner and is granted PRSFS_INSERT permission on the directory. IBM AFS 3.2 altered the cache manager permission checks but failed to correct the permission checks in the AFS server. As of this writing, all IBM AFS derived servers continue to return EACCES in preference to EEXIST when these conditions are met. Bug reports have been filed with all implementations.
As an unintended side effect, the Bash fallback logic also undermines the Linux kernel protections against O_CREAT opening FIFOs and regular files not owned by the user in world writeable sticky directories - unless the owner is the same as that of the directory - as was added in commit 30aba6656f61e ("namei: allow restricted O_CREAT of FIFOs and regular files").
As a result the Bash fallback logic masks an incompatibility between the ownership checks performed by may_create_in_sticky() and network filesystems such as AFS where the uid namespace is disjoint from the uid namespace of the local system.
However, the bash work around has been removed in bash-5.3[2].
Fix this in the kernel by using a preceding patch that allows the user ID comparisons to be overridden by:
(1) Implement the ->is_owned_by_me() inode op for kafs to determine if the caller owns the file by checking to see if the server indicated the ADMINISTER bit was set in the access rights returned by the FS.FetchStatus and suchlike instead of checking the i_uid to current_fsuid().
Unfortunately, this check doesn't work for directories, but none of the ops should require that.
Note that anonymous accesses to AFS will never see the ADMINISTER bit being set and so will not be perceived as owning an anonymously-owned file.
(2) Implement the ->have_same_owner() inode op, for kafs to compare the AFS owner IDs retrieved by FS.FetchStatus (which are 64-bit integers with AuriStor's YFS server and, as such, won't fit in a kuid_t).
Note that whilst an anonymously-owned file will match an anonymously-owned parent directory, an anonymously-owned directory cannot have the sticky bit set.
This can be tested by creating a sticky directory (the user must have a token to do this) and creating a file in it. Then strace bash doing "echo foo >>file" and look at whether bash does a single, successful O_CREAT open on the file or whether that one fails and then bash does one without O_CREAT that succeeds.
--- fs/afs/dir.c | 2 ++ fs/afs/file.c | 2 ++ fs/afs/inode.c | 1 + fs/afs/internal.h | 3 +++ fs/afs/security.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ fs/afs/symlink.c | 2 ++ 6 files changed, 56 insertions(+)
fs/afs/dir.c +2 -0
@@ -69,6 +69,8 @@ const struct inode_operations afs_dir_inode_operations = { .permission = afs_permission, .getattr = afs_getattr, .setattr = afs_setattr, + .is_owned_by_me = afs_is_owned_by_me, + .have_same_owner = afs_have_same_owner, };const struct address_space_operations afs_dir_aops = {
fs/afs/file.c +2 -0
@@ -49,6 +49,8 @@ const struct inode_operations afs_file_inode_operations = { .getattr = afs_getattr, .setattr = afs_setattr, .permission = afs_permission, + .is_owned_by_me = afs_is_owned_by_me, + .have_same_owner = afs_have_same_owner, };const struct address_space_operations afs_file_aops = {
fs/afs/inode.c +1 -0
@@ -91,6 +91,7 @@ static int afs_inode_init_from_status(struct afs_operation *op, inode->i_flags |=S_NOATIME ; inode->i_uid =make_kuid (&init_user_ns, status->owner); inode->i_gid =make_kgid (&init_user_ns, status->group); + inode->i_opflags |=IOP_OWNERSHIP_OVERRIDE ;set_nlink (&vnode->netfs.inode, status->nlink);i_size_write (inode, status->size);
fs/afs/internal.h +3 -0
@@ -1531,6 +1531,9 @@ extern struct key *afs_request_key(struct afs_cell *);extern struct key *afs_request_key_rcu (struct afs_cell *);extern int afs_check_permit (struct afs_vnode *,struct key *,afs_access_t *);extern int afs_permission (struct mnt_idmap *,struct inode *,int ); +int afs_is_owned_by_me (struct mnt_idmap *idmap,struct inode *inode); +int afs_have_same_owner (struct mnt_idmap *idmap,struct inode *inode1, +struct inode *inode2); extern void __exit afs_clean_up_permit_cache(void);/*
fs/afs/security.c +46 -0
52 lines not on this page · raw message
fs/afs/symlink.c +2 -0
@@ -271,6 +271,8 @@ int afs_symlink_writepages(struct address_space *mapping,const struct inode_operations afs_symlink_inode_operations = { .get_link = afs_get_link, .readlink = afs_readlink, + .is_owned_by_me = afs_is_owned_by_me, + .have_same_owner = afs_have_same_owner, };const struct address_space_operations afs_symlink_aops = {