Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

fhandle: relax open_by_handle_at() permission checks

A current limitation of open_by_handle_at() is that it's currently not possible
to use it from within containers at all because we require CAP_DAC_READ_SEARCH
in the initial namespace. That's unfortunate because there are scenarios where
using open_by_handle_at() from within containers.

Two examples:

(1) cgroupfs allows to encode cgroups to file handles and reopen them with
open_by_handle_at().
(2) Fanotify allows placing filesystem watches they currently aren't usable in
containers because the returned file handles cannot be used.

Here's a proposal for relaxing the permission check for open_by_handle_at().

(1) Opening file handles when the caller has privileges over the filesystem
(1.1) The caller has an unobstructed view of the filesystem.
(1.2) The caller has permissions to follow a path to the file handle.

This doesn't address the problem of opening a file handle when only a portion
of a filesystem is exposed as is common in containers by e.g., bind-mounting a
subtree. The proposal to solve this use-case is:

(2) Opening file handles when the caller has privileges over a subtree
(2.1) The caller is able to reach the file from the provided mount fd.
(2.2) The caller has permissions to construct an unobstructed path to the
file handle.
(2.3) The caller has permissions to follow a path to the file handle.

The relaxed permission checks are currently restricted to directory file
handles which are what both cgroupfs and fanotify need. Handling disconnected
non-directory file handles would lead to a potentially non-deterministic api.
If a disconnected non-directory file handle is provided we may fail to decode
a valid path that we could use for permission checking. That in itself isn't a
problem as we would just return EACCES in that case. However, confusion may
arise if a non-disconnected dentry ends up in the cache later and those opening
the file handle would suddenly succeed.

* It's potentially possible to use timing information (side-channel) to infer
whether a given inode exists. I don't think that's particularly
problematic. Thanks to Jann for bringing this to my attention.

* An unrelated note (IOW, these are thoughts that apply to
open_by_handle_at() generically and are unrelated to the changes here):
Jann pointed out that we should verify whether deleted files could
potentially be reopened through open_by_handle_at(). I don't think that's
possible though.

Another potential thing to check is whether open_by_handle_at() could be
abused to open internal stuff like memfds or gpu stuff. I don't think so
but I haven't had the time to completely verify this.

This dates back to discussions Amir and I had quite some time ago and thanks to
him for providing a lot of details around the export code and related patches!

Link: https://lore.kernel.org/r/20240524-vfs-open_by_handle_at-v1-1-3d4b7d22736b@kernel.org
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Christian Brauner <brauner@kernel.org>

+159 -49
+7 -2
fs/exportfs/expfs.c
··· 427 427 428 428 struct dentry * 429 429 exportfs_decode_fh_raw(struct vfsmount *mnt, struct fid *fid, int fh_len, 430 - int fileid_type, 430 + int fileid_type, unsigned int flags, 431 431 int (*acceptable)(void *, struct dentry *), 432 432 void *context) 433 433 { ··· 444 444 result = nop->fh_to_dentry(mnt->mnt_sb, fid, fh_len, fileid_type); 445 445 if (IS_ERR_OR_NULL(result)) 446 446 return result; 447 + 448 + if ((flags & EXPORT_FH_DIR_ONLY) && !d_is_dir(result)) { 449 + err = -ENOTDIR; 450 + goto err_result; 451 + } 447 452 448 453 /* 449 454 * If no acceptance criteria was specified by caller, a disconnected ··· 586 581 { 587 582 struct dentry *ret; 588 583 589 - ret = exportfs_decode_fh_raw(mnt, fid, fh_len, fileid_type, 584 + ret = exportfs_decode_fh_raw(mnt, fid, fh_len, fileid_type, 0, 590 585 acceptable, context); 591 586 if (IS_ERR_OR_NULL(ret)) { 592 587 if (ret == ERR_PTR(-ENOMEM))
+147 -45
fs/fhandle.c
··· 115 115 return err; 116 116 } 117 117 118 - static struct vfsmount *get_vfsmount_from_fd(int fd) 118 + static int get_path_from_fd(int fd, struct path *root) 119 119 { 120 - struct vfsmount *mnt; 121 - 122 120 if (fd == AT_FDCWD) { 123 121 struct fs_struct *fs = current->fs; 124 122 spin_lock(&fs->lock); 125 - mnt = mntget(fs->pwd.mnt); 123 + *root = fs->pwd; 124 + path_get(root); 126 125 spin_unlock(&fs->lock); 127 126 } else { 128 127 struct fd f = fdget(fd); 129 128 if (!f.file) 130 - return ERR_PTR(-EBADF); 131 - mnt = mntget(f.file->f_path.mnt); 129 + return -EBADF; 130 + *root = f.file->f_path; 131 + path_get(root); 132 132 fdput(f); 133 133 } 134 - return mnt; 134 + 135 + return 0; 135 136 } 137 + 138 + enum handle_to_path_flags { 139 + HANDLE_CHECK_PERMS = (1 << 0), 140 + HANDLE_CHECK_SUBTREE = (1 << 1), 141 + }; 142 + 143 + struct handle_to_path_ctx { 144 + struct path root; 145 + enum handle_to_path_flags flags; 146 + unsigned int fh_flags; 147 + }; 136 148 137 149 static int vfs_dentry_acceptable(void *context, struct dentry *dentry) 138 150 { 139 - return 1; 140 - } 141 - 142 - static int do_handle_to_path(int mountdirfd, struct file_handle *handle, 143 - struct path *path) 144 - { 151 + struct handle_to_path_ctx *ctx = context; 152 + struct user_namespace *user_ns = current_user_ns(); 153 + struct dentry *d, *root = ctx->root.dentry; 154 + struct mnt_idmap *idmap = mnt_idmap(ctx->root.mnt); 145 155 int retval = 0; 146 - int handle_dwords; 147 156 148 - path->mnt = get_vfsmount_from_fd(mountdirfd); 149 - if (IS_ERR(path->mnt)) { 150 - retval = PTR_ERR(path->mnt); 151 - goto out_err; 157 + if (!root) 158 + return 1; 159 + 160 + /* Old permission model with global CAP_DAC_READ_SEARCH. */ 161 + if (!ctx->flags) 162 + return 1; 163 + 164 + /* 165 + * It's racy as we're not taking rename_lock but we're able to ignore 166 + * permissions and we just need an approximation whether we were able 167 + * to follow a path to the file. 168 + * 169 + * It's also potentially expensive on some filesystems especially if 170 + * there is a deep path. 171 + */ 172 + d = dget(dentry); 173 + while (d != root && !IS_ROOT(d)) { 174 + struct dentry *parent = dget_parent(d); 175 + 176 + /* 177 + * We know that we have the ability to override DAC permissions 178 + * as we've verified this earlier via CAP_DAC_READ_SEARCH. But 179 + * we also need to make sure that there aren't any unmapped 180 + * inodes in the path that would prevent us from reaching the 181 + * file. 182 + */ 183 + if (!privileged_wrt_inode_uidgid(user_ns, idmap, 184 + d_inode(parent))) { 185 + dput(d); 186 + dput(parent); 187 + return retval; 188 + } 189 + 190 + dput(d); 191 + d = parent; 152 192 } 153 - /* change the handle size to multiple of sizeof(u32) */ 154 - handle_dwords = handle->handle_bytes >> 2; 155 - path->dentry = exportfs_decode_fh(path->mnt, 156 - (struct fid *)handle->f_handle, 157 - handle_dwords, handle->handle_type, 158 - vfs_dentry_acceptable, NULL); 159 - if (IS_ERR(path->dentry)) { 160 - retval = PTR_ERR(path->dentry); 161 - goto out_mnt; 162 - } 163 - return 0; 164 - out_mnt: 165 - mntput(path->mnt); 166 - out_err: 193 + 194 + if (!(ctx->flags & HANDLE_CHECK_SUBTREE) || d == root) 195 + retval = 1; 196 + WARN_ON_ONCE(d != root && d != root->d_sb->s_root); 197 + dput(d); 167 198 return retval; 168 199 } 169 200 201 + static int do_handle_to_path(struct file_handle *handle, struct path *path, 202 + struct handle_to_path_ctx *ctx) 203 + { 204 + int handle_dwords; 205 + struct vfsmount *mnt = ctx->root.mnt; 206 + 207 + /* change the handle size to multiple of sizeof(u32) */ 208 + handle_dwords = handle->handle_bytes >> 2; 209 + path->dentry = exportfs_decode_fh_raw(mnt, 210 + (struct fid *)handle->f_handle, 211 + handle_dwords, handle->handle_type, 212 + ctx->fh_flags, 213 + vfs_dentry_acceptable, ctx); 214 + if (IS_ERR_OR_NULL(path->dentry)) { 215 + if (path->dentry == ERR_PTR(-ENOMEM)) 216 + return -ENOMEM; 217 + return -ESTALE; 218 + } 219 + path->mnt = mntget(mnt); 220 + return 0; 221 + } 222 + 223 + /* 224 + * Allow relaxed permissions of file handles if the caller has the 225 + * ability to mount the filesystem or create a bind-mount of the 226 + * provided @mountdirfd. 227 + * 228 + * In both cases the caller may be able to get an unobstructed way to 229 + * the encoded file handle. If the caller is only able to create a 230 + * bind-mount we need to verify that there are no locked mounts on top 231 + * of it that could prevent us from getting to the encoded file. 232 + * 233 + * In principle, locked mounts can prevent the caller from mounting the 234 + * filesystem but that only applies to procfs and sysfs neither of which 235 + * support decoding file handles. 236 + */ 237 + static inline bool may_decode_fh(struct handle_to_path_ctx *ctx, 238 + unsigned int o_flags) 239 + { 240 + struct path *root = &ctx->root; 241 + 242 + /* 243 + * Restrict to O_DIRECTORY to provide a deterministic API that avoids a 244 + * confusing api in the face of disconnected non-dir dentries. 245 + * 246 + * There's only one dentry for each directory inode (VFS rule)... 247 + */ 248 + if (!(o_flags & O_DIRECTORY)) 249 + return false; 250 + 251 + if (ns_capable(root->mnt->mnt_sb->s_user_ns, CAP_SYS_ADMIN)) 252 + ctx->flags = HANDLE_CHECK_PERMS; 253 + else if (is_mounted(root->mnt) && 254 + ns_capable(real_mount(root->mnt)->mnt_ns->user_ns, 255 + CAP_SYS_ADMIN) && 256 + !has_locked_children(real_mount(root->mnt), root->dentry)) 257 + ctx->flags = HANDLE_CHECK_PERMS | HANDLE_CHECK_SUBTREE; 258 + else 259 + return false; 260 + 261 + /* Are we able to override DAC permissions? */ 262 + if (!ns_capable(current_user_ns(), CAP_DAC_READ_SEARCH)) 263 + return false; 264 + 265 + ctx->fh_flags = EXPORT_FH_DIR_ONLY; 266 + return true; 267 + } 268 + 170 269 static int handle_to_path(int mountdirfd, struct file_handle __user *ufh, 171 - struct path *path) 270 + struct path *path, unsigned int o_flags) 172 271 { 173 272 int retval = 0; 174 273 struct file_handle f_handle; 175 274 struct file_handle *handle = NULL; 275 + struct handle_to_path_ctx ctx = {}; 176 276 177 - /* 178 - * With handle we don't look at the execute bit on the 179 - * directory. Ideally we would like CAP_DAC_SEARCH. 180 - * But we don't have that 181 - */ 182 - if (!capable(CAP_DAC_READ_SEARCH)) { 183 - retval = -EPERM; 277 + retval = get_path_from_fd(mountdirfd, &ctx.root); 278 + if (retval) 184 279 goto out_err; 280 + 281 + if (!capable(CAP_DAC_READ_SEARCH) && !may_decode_fh(&ctx, o_flags)) { 282 + retval = -EPERM; 283 + goto out_path; 185 284 } 285 + 186 286 if (copy_from_user(&f_handle, ufh, sizeof(struct file_handle))) { 187 287 retval = -EFAULT; 188 - goto out_err; 288 + goto out_path; 189 289 } 190 290 if ((f_handle.handle_bytes > MAX_HANDLE_SZ) || 191 291 (f_handle.handle_bytes == 0)) { 192 292 retval = -EINVAL; 193 - goto out_err; 293 + goto out_path; 194 294 } 195 295 handle = kmalloc(struct_size(handle, f_handle, f_handle.handle_bytes), 196 296 GFP_KERNEL); 197 297 if (!handle) { 198 298 retval = -ENOMEM; 199 - goto out_err; 299 + goto out_path; 200 300 } 201 301 /* copy the full handle */ 202 302 *handle = f_handle; ··· 307 207 goto out_handle; 308 208 } 309 209 310 - retval = do_handle_to_path(mountdirfd, handle, path); 210 + retval = do_handle_to_path(handle, path, &ctx); 311 211 312 212 out_handle: 313 213 kfree(handle); 214 + out_path: 215 + path_put(&ctx.root); 314 216 out_err: 315 217 return retval; 316 218 } ··· 325 223 struct file *file; 326 224 int fd; 327 225 328 - retval = handle_to_path(mountdirfd, ufh, &path); 226 + retval = handle_to_path(mountdirfd, ufh, &path, open_flag); 329 227 if (retval) 330 228 return retval; 331 229
+1
fs/mount.h
··· 152 152 } 153 153 154 154 extern void mnt_cursor_del(struct mnt_namespace *ns, struct mount *cursor); 155 + bool has_locked_children(struct mount *mnt, struct dentry *dentry);
+1 -1
fs/namespace.c
··· 2078 2078 namespace_unlock(); 2079 2079 } 2080 2080 2081 - static bool has_locked_children(struct mount *mnt, struct dentry *dentry) 2081 + bool has_locked_children(struct mount *mnt, struct dentry *dentry) 2082 2082 { 2083 2083 struct mount *child; 2084 2084
+1 -1
fs/nfsd/nfsfh.c
··· 247 247 dentry = dget(exp->ex_path.dentry); 248 248 else { 249 249 dentry = exportfs_decode_fh_raw(exp->ex_path.mnt, fid, 250 - data_left, fileid_type, 250 + data_left, fileid_type, 0, 251 251 nfsd_acceptable, exp); 252 252 if (IS_ERR_OR_NULL(dentry)) { 253 253 trace_nfsd_set_fh_dentry_badhandle(rqstp, fhp,
+2
include/linux/exportfs.h
··· 158 158 159 159 #define EXPORT_FH_CONNECTABLE 0x1 /* Encode file handle with parent */ 160 160 #define EXPORT_FH_FID 0x2 /* File handle may be non-decodeable */ 161 + #define EXPORT_FH_DIR_ONLY 0x4 /* Only decode file handle for a directory */ 161 162 162 163 /** 163 164 * struct export_operations - for nfsd to communicate with file systems ··· 306 305 extern struct dentry *exportfs_decode_fh_raw(struct vfsmount *mnt, 307 306 struct fid *fid, int fh_len, 308 307 int fileid_type, 308 + unsigned int flags, 309 309 int (*acceptable)(void *, struct dentry *), 310 310 void *context); 311 311 extern struct dentry *exportfs_decode_fh(struct vfsmount *mnt, struct fid *fid,