| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * FUSE passthrough to backing file. |
| * |
| * Copyright (c) 2023 CTERA Networks. |
| */ |
| |
| #include "fuse_i.h" |
| |
| #include <linux/file.h> |
| |
| struct fuse_backing *fuse_backing_get(struct fuse_backing *fb) |
| { |
| if (fb && refcount_inc_not_zero(&fb->count)) |
| return fb; |
| return NULL; |
| } |
| |
| static void fuse_backing_free(struct fuse_backing *fb) |
| { |
| pr_debug("%s: fb=0x%p\n", __func__, fb); |
| |
| if (fb->file) |
| fput(fb->file); |
| put_cred(fb->cred); |
| kfree_rcu(fb, rcu); |
| } |
| |
| void fuse_backing_put(struct fuse_backing *fb) |
| { |
| if (fb && refcount_dec_and_test(&fb->count)) |
| fuse_backing_free(fb); |
| } |
| |
| void fuse_backing_files_init(struct fuse_conn *fc) |
| { |
| idr_init(&fc->backing_files_map); |
| } |
| |
| static int fuse_backing_id_alloc(struct fuse_conn *fc, struct fuse_backing *fb) |
| { |
| int id; |
| |
| idr_preload(GFP_KERNEL); |
| spin_lock(&fc->lock); |
| /* FIXME: xarray might be space inefficient */ |
| id = idr_alloc_cyclic(&fc->backing_files_map, fb, 1, 0, GFP_ATOMIC); |
| spin_unlock(&fc->lock); |
| idr_preload_end(); |
| |
| WARN_ON_ONCE(id == 0); |
| return id; |
| } |
| |
| static struct fuse_backing *fuse_backing_id_remove(struct fuse_conn *fc, |
| int id) |
| { |
| struct fuse_backing *fb; |
| |
| spin_lock(&fc->lock); |
| fb = idr_remove(&fc->backing_files_map, id); |
| spin_unlock(&fc->lock); |
| |
| return fb; |
| } |
| |
| static int fuse_backing_id_free(int id, void *p, void *data) |
| { |
| struct fuse_backing *fb = p; |
| |
| WARN_ON_ONCE(refcount_read(&fb->count) != 1); |
| fuse_backing_free(fb); |
| return 0; |
| } |
| |
| void fuse_backing_files_free(struct fuse_conn *fc) |
| { |
| idr_for_each(&fc->backing_files_map, fuse_backing_id_free, NULL); |
| idr_destroy(&fc->backing_files_map); |
| } |
| |
| int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map) |
| { |
| struct file *file; |
| struct super_block *backing_sb; |
| struct fuse_backing *fb = NULL; |
| int res; |
| |
| pr_debug("%s: fd=%d flags=0x%x\n", __func__, map->fd, map->flags); |
| |
| /* TODO: relax CAP_SYS_ADMIN once backing files are visible to lsof */ |
| res = -EPERM; |
| if (!fc->passthrough || !capable(CAP_SYS_ADMIN)) |
| goto out; |
| |
| res = -EINVAL; |
| if (map->flags || map->padding) |
| goto out; |
| |
| file = fget_raw(map->fd); |
| res = -EBADF; |
| if (!file) |
| goto out; |
| |
| /* read/write/splice/mmap passthrough only relevant for regular files */ |
| res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL; |
| if (!d_is_reg(file->f_path.dentry)) |
| goto out_fput; |
| |
| backing_sb = file_inode(file)->i_sb; |
| res = -ELOOP; |
| if (backing_sb->s_stack_depth >= fc->max_stack_depth) |
| goto out_fput; |
| |
| fb = kmalloc(sizeof(struct fuse_backing), GFP_KERNEL); |
| res = -ENOMEM; |
| if (!fb) |
| goto out_fput; |
| |
| fb->file = file; |
| fb->cred = prepare_creds(); |
| refcount_set(&fb->count, 1); |
| |
| res = fuse_backing_id_alloc(fc, fb); |
| if (res < 0) { |
| fuse_backing_free(fb); |
| fb = NULL; |
| } |
| |
| out: |
| pr_debug("%s: fb=0x%p, ret=%i\n", __func__, fb, res); |
| |
| return res; |
| |
| out_fput: |
| fput(file); |
| goto out; |
| } |
| |
| int fuse_backing_close(struct fuse_conn *fc, int backing_id) |
| { |
| struct fuse_backing *fb = NULL; |
| int err; |
| |
| pr_debug("%s: backing_id=%d\n", __func__, backing_id); |
| |
| /* TODO: relax CAP_SYS_ADMIN once backing files are visible to lsof */ |
| err = -EPERM; |
| if (!fc->passthrough || !capable(CAP_SYS_ADMIN)) |
| goto out; |
| |
| err = -EINVAL; |
| if (backing_id <= 0) |
| goto out; |
| |
| err = -ENOENT; |
| fb = fuse_backing_id_remove(fc, backing_id); |
| if (!fb) |
| goto out; |
| |
| fuse_backing_put(fb); |
| err = 0; |
| out: |
| pr_debug("%s: fb=0x%p, err=%i\n", __func__, fb, err); |
| |
| return err; |
| } |
| |
| struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, int backing_id) |
| { |
| struct fuse_backing *fb; |
| |
| rcu_read_lock(); |
| fb = idr_find(&fc->backing_files_map, backing_id); |
| fb = fuse_backing_get(fb); |
| rcu_read_unlock(); |
| |
| return fb; |
| } |