blob: 8c21a8a74efb9c8cb61c370f964a6f67d356d563 [file]
// SPDX-License-Identifier: GPL-2.0
/*
* FUSE passthrough to backing file.
*
* Copyright (c) 2023 CTERA Networks.
*/
#include "dev.h"
#include "fuse_i.h"
#include <linux/file.h>
#include <linux/dax.h>
#include <linux/rhashtable.h>
static 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);
switch (fb->type) {
case FUSE_BACKING_PATH:
path_put(&fb->path);
put_cred(fb->cred);
break;
case FUSE_BACKING_DAXDEV:
fs_put_dax(fb->dax_dev, fb);
break;
case FUSE_BACKING_EXTMAP:
fuse_ext_map_destroy(fb);
break;
}
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();
if (id > 0)
fb->backing_id = id;
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);
if (fb)
fb->backing_id = 0;
return fb;
}
static const struct rhashtable_params fuse_backing_prm = {
.head_offset = offsetof(struct fuse_backing, hash_node),
.key_offset = offsetof(struct fuse_backing, backing_id),
.key_len = sizeof_field(struct fuse_backing, backing_id),
};
int fuse_backing_add_64(struct fuse_conn *fc, struct fuse_backing *fb)
{
return rhashtable_lookup_insert_fast(&fc->backing_64_ht, &fb->hash_node, fuse_backing_prm);
}
int fuse_backing_close_64(struct fuse_conn *fc, u64 backing_id)
{
struct fuse_backing *fb;
int err;
if (!fc->backing_id_64)
return -EINVAL;
scoped_guard(spinlock, &fc->lock) {
fb = rhashtable_lookup_fast(&fc->backing_64_ht, &backing_id, fuse_backing_prm);
if (!fb)
return -ENOENT;
err = rhashtable_remove_fast(&fc->backing_64_ht, &fb->hash_node, fuse_backing_prm);
WARN_ON(err);
}
fb->backing_id = 0;
fuse_backing_put(fb);
return 0;
}
static int fuse_dax_notify_failure(struct dax_device *daxdev, u64 offset, u64 len, int mf_flags)
{
struct fuse_backing *fb;
guard(rcu)();
fb = dax_holder(daxdev);
if (fb)
fb->dax_error = true;
return 0;
}
static const struct dax_holder_operations fuse_dax_holder_ops = {
.notify_failure = fuse_dax_notify_failure,
};
static int fuse_backing_open_file(struct fuse_conn *fc, struct fuse_backing *fb, struct file *file)
{
struct inode *inode = file_inode(file);
struct dax_device *daxdev;
int err;
switch (inode->i_mode & S_IFMT) {
case S_IFREG:
/* TODO: relax CAP_SYS_ADMIN once backing files are visible to lsof */
if (!fc->passthrough || !capable(CAP_SYS_ADMIN))
return -EPERM;
if (inode->i_sb->s_stack_depth >= fc->max_stack_depth)
return -ELOOP;
fb->type = FUSE_BACKING_PATH;
fb->path = file->f_path;
path_get(&fb->path);
fb->cred = get_current_cred();
return 0;
case S_IFCHR:
if (!fc->passthrough || !fc->backing_id_64)
return -EINVAL;
if (!IS_REACHABLE(CONFIG_DEV_DAX_FSDEV))
return -EINVAL;
daxdev = fsdev_dax_from_file(file);
if (!daxdev)
return -EINVAL;
err = -EPERM;
if (capable(CAP_SYS_RAWIO)) {
err = fs_dax_get(daxdev, fb, &fuse_dax_holder_ops);
if (!err) {
fb->type = FUSE_BACKING_DAXDEV;
fb->dax_dev = daxdev;
}
}
put_dax(daxdev);
return err;
case S_IFDIR:
return -EISDIR;
default:
return -EINVAL;
}
}
static struct fuse_backing *fuse_backing_new(struct fuse_conn *fc, int fd)
{
struct fuse_backing *fb __free(kfree) = kzalloc_obj(*fb);
int err;
if (!fb)
return ERR_PTR(-ENOMEM);
CLASS(fd_raw, f)(fd);
if (fd_empty(f))
return ERR_PTR(-EBADF);
err = fuse_backing_open_file(fc, fb, fd_file(f));
if (err)
return ERR_PTR(err);
refcount_set(&fb->count, 1);
return_ptr(fb);
}
int fuse_backing_open_64(struct fuse_conn *fc, struct fuse_backing_create_in *map)
{
struct fuse_backing *fb;
int res;
if (map->padding || map->spare[0] || map->spare[1] || !map->backing_id)
return -EINVAL;
if (!fc->backing_id_64)
return -EINVAL;
fb = fuse_backing_new(fc, map->fd);
if (IS_ERR(fb))
return PTR_ERR(fb);
fb->backing_id = map->backing_id;
res = fuse_backing_add_64(fc, fb);
if (res < 0)
fuse_backing_free(fb);
return res;
}
int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
{
struct fuse_backing *fb = NULL;
int res;
pr_debug("%s: fd=%d flags=0x%x\n", __func__, map->fd, map->flags);
res = -EINVAL;
if (map->flags || map->padding)
goto out;
if (fc->backing_id_64)
goto out;
fb = fuse_backing_new(fc, map->fd);
res = PTR_ERR(fb);
if (!IS_ERR(fb)) {
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;
}
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);
if (fc->backing_id_64)
return -EINVAL;
/* 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;
}
bool fuse_backing_is_dax(struct fuse_backing *fb)
{
switch (fb->type) {
case FUSE_BACKING_PATH:
return false;
case FUSE_BACKING_DAXDEV:
return true;
case FUSE_BACKING_EXTMAP:
return fuse_ext_map_is_dax(fb);
default:
WARN_ON(1);
return false;
}
}
struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, u64 backing_id)
{
struct fuse_backing *fb;
guard(rcu)();
if (!fc->backing_id_64)
fb = idr_find(&fc->backing_files_map, backing_id);
else
fb = rhashtable_lookup(&fc->backing_64_ht, &backing_id, fuse_backing_prm);
return fuse_backing_get(fb);
}
static void fuse_backing_check_free(struct fuse_backing *fb)
{
WARN_ON_ONCE(refcount_read(&fb->count) != 1);
fuse_backing_free(fb);
}
static int fuse_backing_idr_free(int id, void *p, void *data)
{
fuse_backing_check_free(p);
return 0;
}
static void fuse_backing_rht_free(void *p, void *data)
{
fuse_backing_check_free(p);
}
void fuse_backing_files_free(struct fuse_conn *fc)
{
struct rhashtable_iter iter;
struct fuse_backing *fb;
if (!fc->backing_id_64) {
idr_for_each(&fc->backing_files_map, fuse_backing_idr_free, NULL);
idr_destroy(&fc->backing_files_map);
return;
}
/*
* extents are referencing other backings, put these refs before
* destroying the backings themselves
*/
rhashtable_walk_enter(&fc->backing_64_ht, &iter);
rhashtable_walk_start(&iter);
while ((fb = rhashtable_walk_next(&iter))) {
if (IS_ERR(fb)) {
if (PTR_ERR(fb) == -EAGAIN)
continue;
break;
}
if (fb->type == FUSE_BACKING_EXTMAP && fb->extents) {
rhashtable_walk_stop(&iter);
fuse_ext_map_destroy(fb);
rhashtable_walk_start(&iter);
}
}
rhashtable_walk_stop(&iter);
rhashtable_walk_exit(&iter);
rhashtable_free_and_destroy(&fc->backing_64_ht, fuse_backing_rht_free, NULL);
}
void fuse_backing_files_init_64(struct fuse_conn *fc)
{
rhashtable_init(&fc->backing_64_ht, &fuse_backing_prm);
}