Skip to content

Commit 620c266

Browse files
committed
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>
1 parent ef44c8a commit 620c266

File tree

6 files changed

+152
-42
lines changed

6 files changed

+152
-42
lines changed

fs/exportfs/expfs.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ EXPORT_SYMBOL_GPL(exportfs_encode_fh);
427427

428428
struct dentry *
429429
exportfs_decode_fh_raw(struct vfsmount *mnt, struct fid *fid, int fh_len,
430-
int fileid_type,
430+
int fileid_type, unsigned int flags,
431431
int (*acceptable)(void *, struct dentry *),
432432
void *context)
433433
{
@@ -445,6 +445,11 @@ exportfs_decode_fh_raw(struct vfsmount *mnt, struct fid *fid, int fh_len,
445445
if (IS_ERR_OR_NULL(result))
446446
return result;
447447

448+
if ((flags & EXPORT_FH_DIR_ONLY) && !d_is_dir(result)) {
449+
err = -ENOTDIR;
450+
goto err_result;
451+
}
452+
448453
/*
449454
* If no acceptance criteria was specified by caller, a disconnected
450455
* dentry is also accepatable. Callers may use this mode to query if
@@ -581,7 +586,7 @@ struct dentry *exportfs_decode_fh(struct vfsmount *mnt, struct fid *fid,
581586
{
582587
struct dentry *ret;
583588

584-
ret = exportfs_decode_fh_raw(mnt, fid, fh_len, fileid_type,
589+
ret = exportfs_decode_fh_raw(mnt, fid, fh_len, fileid_type, 0,
585590
acceptable, context);
586591
if (IS_ERR_OR_NULL(ret)) {
587592
if (ret == ERR_PTR(-ENOMEM))

fs/fhandle.c

Lines changed: 140 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -115,88 +115,188 @@ SYSCALL_DEFINE5(name_to_handle_at, int, dfd, const char __user *, name,
115115
return err;
116116
}
117117

118-
static struct vfsmount *get_vfsmount_from_fd(int fd)
118+
static int get_path_from_fd(int fd, struct path *root)
119119
{
120-
struct vfsmount *mnt;
121-
122120
if (fd == AT_FDCWD) {
123121
struct fs_struct *fs = current->fs;
124122
spin_lock(&fs->lock);
125-
mnt = mntget(fs->pwd.mnt);
123+
*root = fs->pwd;
124+
path_get(root);
126125
spin_unlock(&fs->lock);
127126
} else {
128127
struct fd f = fdget(fd);
129128
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);
132132
fdput(f);
133133
}
134-
return mnt;
134+
135+
return 0;
135136
}
136137

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+
};
148+
137149
static int vfs_dentry_acceptable(void *context, struct dentry *dentry)
138150
{
139-
return 1;
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);
155+
int retval = 0;
156+
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;
192+
}
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);
198+
return retval;
140199
}
141200

142-
static int do_handle_to_path(int mountdirfd, struct file_handle *handle,
143-
struct path *path)
201+
static int do_handle_to_path(struct file_handle *handle, struct path *path,
202+
struct handle_to_path_ctx *ctx)
144203
{
145-
int retval = 0;
146204
int handle_dwords;
205+
struct vfsmount *mnt = ctx->root.mnt;
147206

148-
path->mnt = get_vfsmount_from_fd(mountdirfd);
149-
if (IS_ERR(path->mnt)) {
150-
retval = PTR_ERR(path->mnt);
151-
goto out_err;
152-
}
153207
/* change the handle size to multiple of sizeof(u32) */
154208
handle_dwords = handle->handle_bytes >> 2;
155-
path->dentry = exportfs_decode_fh(path->mnt,
209+
path->dentry = exportfs_decode_fh_raw(mnt,
156210
(struct fid *)handle->f_handle,
157211
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;
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;
162218
}
219+
path->mnt = mntget(mnt);
163220
return 0;
164-
out_mnt:
165-
mntput(path->mnt);
166-
out_err:
167-
return retval;
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;
168267
}
169268

170269
static int handle_to_path(int mountdirfd, struct file_handle __user *ufh,
171-
struct path *path)
270+
struct path *path, unsigned int o_flags)
172271
{
173272
int retval = 0;
174273
struct file_handle f_handle;
175274
struct file_handle *handle = NULL;
275+
struct handle_to_path_ctx ctx = {};
176276

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)
184279
goto out_err;
280+
281+
if (!capable(CAP_DAC_READ_SEARCH) && !may_decode_fh(&ctx, o_flags)) {
282+
retval = -EPERM;
283+
goto out_path;
185284
}
285+
186286
if (copy_from_user(&f_handle, ufh, sizeof(struct file_handle))) {
187287
retval = -EFAULT;
188-
goto out_err;
288+
goto out_path;
189289
}
190290
if ((f_handle.handle_bytes > MAX_HANDLE_SZ) ||
191291
(f_handle.handle_bytes == 0)) {
192292
retval = -EINVAL;
193-
goto out_err;
293+
goto out_path;
194294
}
195295
handle = kmalloc(struct_size(handle, f_handle, f_handle.handle_bytes),
196296
GFP_KERNEL);
197297
if (!handle) {
198298
retval = -ENOMEM;
199-
goto out_err;
299+
goto out_path;
200300
}
201301
/* copy the full handle */
202302
*handle = f_handle;
@@ -207,10 +307,12 @@ static int handle_to_path(int mountdirfd, struct file_handle __user *ufh,
207307
goto out_handle;
208308
}
209309

210-
retval = do_handle_to_path(mountdirfd, handle, path);
310+
retval = do_handle_to_path(handle, path, &ctx);
211311

212312
out_handle:
213313
kfree(handle);
314+
out_path:
315+
path_put(&ctx.root);
214316
out_err:
215317
return retval;
216318
}
@@ -223,7 +325,7 @@ static long do_handle_open(int mountdirfd, struct file_handle __user *ufh,
223325
struct file *file;
224326
int fd;
225327

226-
retval = handle_to_path(mountdirfd, ufh, &path);
328+
retval = handle_to_path(mountdirfd, ufh, &path, open_flag);
227329
if (retval)
228330
return retval;
229331

fs/mount.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,4 @@ static inline void move_from_ns(struct mount *mnt, struct list_head *dt_list)
152152
}
153153

154154
extern void mnt_cursor_del(struct mnt_namespace *ns, struct mount *cursor);
155+
bool has_locked_children(struct mount *mnt, struct dentry *dentry);

fs/namespace.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2078,7 +2078,7 @@ void drop_collected_mounts(struct vfsmount *mnt)
20782078
namespace_unlock();
20792079
}
20802080

2081-
static bool has_locked_children(struct mount *mnt, struct dentry *dentry)
2081+
bool has_locked_children(struct mount *mnt, struct dentry *dentry)
20822082
{
20832083
struct mount *child;
20842084

fs/nfsd/nfsfh.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ static __be32 nfsd_set_fh_dentry(struct svc_rqst *rqstp, struct svc_fh *fhp)
247247
dentry = dget(exp->ex_path.dentry);
248248
else {
249249
dentry = exportfs_decode_fh_raw(exp->ex_path.mnt, fid,
250-
data_left, fileid_type,
250+
data_left, fileid_type, 0,
251251
nfsd_acceptable, exp);
252252
if (IS_ERR_OR_NULL(dentry)) {
253253
trace_nfsd_set_fh_dentry_badhandle(rqstp, fhp,

include/linux/exportfs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ struct fid {
158158

159159
#define EXPORT_FH_CONNECTABLE 0x1 /* Encode file handle with parent */
160160
#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 */
161162

162163
/**
163164
* struct export_operations - for nfsd to communicate with file systems
@@ -305,6 +306,7 @@ static inline int exportfs_encode_fid(struct inode *inode, struct fid *fid,
305306
extern struct dentry *exportfs_decode_fh_raw(struct vfsmount *mnt,
306307
struct fid *fid, int fh_len,
307308
int fileid_type,
309+
unsigned int flags,
308310
int (*acceptable)(void *, struct dentry *),
309311
void *context);
310312
extern struct dentry *exportfs_decode_fh(struct vfsmount *mnt, struct fid *fid,

0 commit comments

Comments
 (0)