From 9f7e57f8d6b084d2d46c41d2e2a7dbfb34b30a0a Mon Sep 17 00:00:00 2001 From: Nikita Shubin Date: Sat, 22 Feb 2025 08:12:11 +0300 Subject: [PATCH] WIP: add guse lowlevel Signed-off-by: Nikita Shubin --- include/fuse_kernel.h | 3 + include/guse_lowlevel.h | 84 +++++++++++ include/meson.build | 3 +- lib/fuse_i.h | 3 + lib/fuse_lowlevel.c | 17 ++- lib/fuse_versionscript | 5 + lib/guse_lowlevel.c | 311 ++++++++++++++++++++++++++++++++++++++++ lib/meson.build | 5 +- 8 files changed, 423 insertions(+), 8 deletions(-) create mode 100644 include/guse_lowlevel.h create mode 100644 lib/guse_lowlevel.c diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index d08b99d..22f0672 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -637,6 +637,9 @@ enum fuse_opcode { /* CUSE specific operations */ CUSE_INIT = 4096, + /* GUSE specific operations */ + GUSE_INIT = 4097, + /* Reserved opcodes: helpful to detect structure endian-ness */ CUSE_INIT_BSWAP_RESERVED = 1048576, /* CUSE_INIT << 8 */ FUSE_INIT_BSWAP_RESERVED = 436207616, /* FUSE_INIT << 24 */ diff --git a/include/guse_lowlevel.h b/include/guse_lowlevel.h new file mode 100644 index 0000000..4dbc303 --- /dev/null +++ b/include/guse_lowlevel.h @@ -0,0 +1,84 @@ +/* + GUSE: GPIO character device in Userspace + Copyright (C) 2025 Nikita Shubin + + This program can be distributed under the terms of the GNU LGPLv2. + See the file COPYING.LIB. + + Read example/cusexmp.c for usages. +*/ + +#ifndef GUSE_LOWLEVEL_H_ +#define GUSE_LOWLEVEL_H_ + +#ifndef FUSE_USE_VERSION +#define FUSE_USE_VERSION 29 +#endif + +#include "fuse_lowlevel.h" + +#include +#include +#include + +#define GUSE_DEVICE_INODE_FLAG BIT_ULL(63) + +#ifdef __cplusplus +extern "C" { +#endif + +struct fuse_session; + +struct guse_info { + unsigned dev_major; + unsigned dev_minor; + unsigned dev_info_argc; + const char **dev_info_argv; + unsigned flags; +}; + +/* + * Most ops behave almost identically to the matching fuse_lowlevel + * ops except that they don't take @ino. + * + * init_done : called after initialization is complete + * read : always direct IO, simultaneous operations allowed + * ioctl : might be in unrestricted mode depending on ci->flags + */ +struct guse_cdev_lowlevel_ops { + void (*init) (void *userdata, struct fuse_conn_info *conn); + void (*init_done) (void *userdata); + void (*destroy) (void *userdata); + void (*open) (fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi); + void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi); + void (*release) (fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi); + void (*ioctl) (fuse_req_t req, fuse_ino_t ino, unsigned int cmd, + void *arg, struct fuse_file_info *fi, unsigned flags, + const void *in_buf, size_t in_bufsz, size_t out_bufsz); + void (*poll) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi, + struct fuse_pollhandle *ph); +}; + +struct fuse_session *guse_lowlevel_new(struct fuse_args *args, + const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *clop, + void *userdata); + +struct fuse_session *guse_lowlevel_setup(int argc, char *argv[], + const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *clop, + int *multithreaded, void *userdata); + +void guse_lowlevel_teardown(struct fuse_session *se); + +int guse_lowlevel_main(int argc, char *argv[], const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *clop, void *userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* GUSE_LOWLEVEL_H_ */ diff --git a/include/meson.build b/include/meson.build index bf67197..e40c438 100644 --- a/include/meson.build +++ b/include/meson.build @@ -1,4 +1,5 @@ libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h', - 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ] + 'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h', + 'guse_lowlevel.h' ] install_headers(libfuse_headers, subdir: 'fuse3') diff --git a/lib/fuse_i.h b/lib/fuse_i.h index ea04c34..74c768d 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -61,6 +61,7 @@ struct fuse_session { struct fuse_lowlevel_ops op; int got_init; struct cuse_data *cuse_data; + struct guse_data *guse_data; void *userdata; uid_t owner; struct fuse_conn_info conn; @@ -186,6 +187,8 @@ void fuse_free_req(fuse_req_t req); void cuse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeide, const void *inarg); +void guse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeide, const void *inarg); + int fuse_start_thread(pthread_t *thread_id, void *(*func)(void *), void *arg); void fuse_buf_free(struct fuse_buf *buf); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e48efa5..613f57f 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2710,6 +2710,7 @@ static struct { [FUSE_COPY_FILE_RANGE] = { do_copy_file_range, "COPY_FILE_RANGE" }, [FUSE_LSEEK] = { do_lseek, "LSEEK" }, [CUSE_INIT] = { cuse_lowlevel_init, "CUSE_INIT" }, + [GUSE_INIT] = { guse_lowlevel_init, "GUSE_INIT" }, }; #define FUSE_MAXOP (sizeof(fuse_ll_ops) / sizeof(fuse_ll_ops[0])) @@ -2809,14 +2810,20 @@ void fuse_session_process_buf_internal(struct fuse_session *se, err = EIO; if (!se->got_init) { - enum fuse_opcode expected; + if (se->guse_data && (in->opcode == GUSE_INIT)) + goto opcode_ok; - expected = se->cuse_data ? CUSE_INIT : FUSE_INIT; - if (in->opcode != expected) - goto reply_err; - } else if (in->opcode == FUSE_INIT || in->opcode == CUSE_INIT) + if (se->cuse_data && (in->opcode == CUSE_INIT)) + goto opcode_ok; + + if (in->opcode == FUSE_INIT) + goto opcode_ok; + + goto reply_err; + } else if (in->opcode == FUSE_INIT || in->opcode == CUSE_INIT || in->opcode == GUSE_INIT) goto reply_err; +opcode_ok: err = EACCES; /* Implement -o allow_root */ if (se->deny_others && in->uid != se->owner && in->uid != 0 && diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 14cbca1..2ae031e 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -203,6 +203,11 @@ FUSE_3.17 { fuse_log_close_syslog; } FUSE_3.12; +FUSE_3.18 { + global: + guse_lowlevel_main; +} FUSE_3.17; + # Local Variables: # indent-tabs-mode: t # End: diff --git a/lib/guse_lowlevel.c b/lib/guse_lowlevel.c new file mode 100644 index 0000000..3c5715c --- /dev/null +++ b/lib/guse_lowlevel.c @@ -0,0 +1,311 @@ +/* + GUSE: Character device in Userspace + Copyright (C) 2025 Nikita Shubin + + This program can be distributed under the terms of the GNU LGPLv2. + See the file COPYING.LIB. +*/ + +#include "fuse_config.h" +#include "guse_lowlevel.h" +#include "fuse_kernel.h" +#include "fuse_i.h" +#include "fuse_opt.h" + +#include +#include +#include +#include +#include +#include + +struct guse_data { + struct guse_cdev_lowlevel_ops glop; + unsigned max_read; + unsigned dev_major; + unsigned dev_minor; + unsigned flags; + unsigned dev_info_len; + char dev_info[]; +}; + +static struct guse_cdev_lowlevel_ops *req_glop(fuse_req_t req) +{ + return &req->se->guse_data->glop; +} + +static size_t guse_pack_info(int argc, const char **argv, char *buf) +{ + size_t size = 0; + int i; + + for (i = 0; i < argc; i++) { + size_t len; + + len = strlen(argv[i]) + 1; + size += len; + if (buf) { + memcpy(buf, argv[i], len); + buf += len; + } + } + + return size; +} + +static struct guse_data *guse_prep_data(const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *glop) +{ + struct guse_data *cd; + size_t dev_info_len; + + dev_info_len = guse_pack_info(ci->dev_info_argc, ci->dev_info_argv, + NULL); + + if (dev_info_len > CUSE_INIT_INFO_MAX) { + fuse_log(FUSE_LOG_ERR, "guse: dev_info (%zu) too large, limit=%u\n", + dev_info_len, CUSE_INIT_INFO_MAX); + return NULL; + } + + cd = calloc(1, sizeof(*cd) + dev_info_len); + if (!cd) { + fuse_log(FUSE_LOG_ERR, "guse: failed to allocate guse_data\n"); + return NULL; + } + + memcpy(&cd->glop, glop, sizeof(cd->glop)); + cd->max_read = 131072; + cd->dev_major = ci->dev_major; + cd->dev_minor = ci->dev_minor; + cd->dev_info_len = dev_info_len; + cd->flags = ci->flags; + guse_pack_info(ci->dev_info_argc, ci->dev_info_argv, cd->dev_info); + + return cd; +} + +struct fuse_session *guse_lowlevel_new(struct fuse_args *args, + const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *glop, + void *userdata) +{ + struct fuse_lowlevel_ops lop; + struct guse_data *cd; + struct fuse_session *se; + + cd = guse_prep_data(ci, glop); + if (!cd) + return NULL; + + memset(&lop, 0, sizeof(lop)); + lop.init = glop->init; + lop.destroy = glop->destroy; + + /* TODO: sanity check */ + + /* TODO: they are all required for normal operations */ + lop.open = glop->open; + lop.read = glop->read; + lop.release = glop->release; + lop.ioctl = glop->ioctl; + lop.poll = glop->poll; + + se = fuse_session_new(args, &lop, sizeof(lop), userdata); + if (!se) + goto out_fail; + + se->guse_data = cd; + + return se; + +out_fail: + free(cd); + return NULL; +} + +static int guse_reply_init(fuse_req_t req, struct cuse_init_out *arg, + char *dev_info, unsigned dev_info_len) +{ + struct iovec iov[3] = { { 0 } }; + + iov[1].iov_base = arg; + iov[1].iov_len = sizeof(struct cuse_init_out); + iov[2].iov_base = dev_info; + iov[2].iov_len = dev_info_len; + + return fuse_send_reply_iov_nofree(req, 0, iov, 3); +} + +void guse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + struct fuse_init_in *arg = (struct fuse_init_in *) inarg; + struct cuse_init_out outarg; + struct fuse_session *se = req->se; + struct guse_data *cd = se->guse_data; + size_t bufsize = se->bufsize; + struct guse_cdev_lowlevel_ops *glop = req_glop(req); + + (void) nodeid; + if (se->debug) { + fuse_log(FUSE_LOG_DEBUG, "GUSE_INIT: %u.%u\n", arg->major, arg->minor); + fuse_log(FUSE_LOG_DEBUG, "flags=0x%08x\n", arg->flags); + } + se->conn.proto_major = arg->major; + se->conn.proto_minor = arg->minor; + se->conn.capable = 0; + se->conn.want = 0; + + if (arg->major < 7) { + fuse_log(FUSE_LOG_ERR, "guse: unsupported protocol version: %u.%u\n", + arg->major, arg->minor); + fuse_reply_err(req, EPROTO); + return; + } + + if (bufsize < FUSE_MIN_READ_BUFFER) { + fuse_log(FUSE_LOG_ERR, "guse: warning: buffer size too small: %zu\n", + bufsize); + bufsize = FUSE_MIN_READ_BUFFER; + } + + bufsize -= 4096; + if (bufsize < se->conn.max_write) + se->conn.max_write = bufsize; + + se->got_init = 1; + if (se->op.init) + se->op.init(se->userdata, &se->conn); + + memset(&outarg, 0, sizeof(outarg)); + outarg.major = FUSE_KERNEL_VERSION; + outarg.minor = FUSE_KERNEL_MINOR_VERSION; + outarg.flags = cd->flags; + outarg.max_read = cd->max_read; + outarg.max_write = se->conn.max_write; + outarg.dev_major = cd->dev_major; + outarg.dev_minor = cd->dev_minor; + + if (se->debug) { + fuse_log(FUSE_LOG_DEBUG, " GUSE_INIT: %u.%u\n", + outarg.major, outarg.minor); + fuse_log(FUSE_LOG_DEBUG, " flags=0x%08x\n", outarg.flags); + fuse_log(FUSE_LOG_DEBUG, " max_read=0x%08x\n", outarg.max_read); + fuse_log(FUSE_LOG_DEBUG, " max_write=0x%08x\n", outarg.max_write); + fuse_log(FUSE_LOG_DEBUG, " dev_major=%u\n", outarg.dev_major); + fuse_log(FUSE_LOG_DEBUG, " dev_minor=%u\n", outarg.dev_minor); + fuse_log(FUSE_LOG_DEBUG, " dev_info: %.*s\n", cd->dev_info_len, + cd->dev_info); + } + + guse_reply_init(req, &outarg, cd->dev_info, cd->dev_info_len); + + if (glop->init_done) + glop->init_done(se->userdata); + + fuse_free_req(req); +} + +struct fuse_session *guse_lowlevel_setup(int argc, char *argv[], + const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *glop, + int *multithreaded, void *userdata) +{ + const char *devname = "/dev/guse"; + static const struct fuse_opt kill_subtype_opts[] = { + FUSE_OPT_KEY("subtype=", FUSE_OPT_KEY_DISCARD), + FUSE_OPT_END + }; + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts opts; + int fd; + int res; + + if (fuse_parse_cmdline(&args, &opts) == -1) + return NULL; + *multithreaded = !opts.singlethread; + + /* Remove subtype= option */ + res = fuse_opt_parse(&args, NULL, kill_subtype_opts, NULL); + if (res == -1) + goto out1; + + /* + * Make sure file descriptors 0, 1 and 2 are open, otherwise chaos + * would ensue. + */ + do { + fd = open("/dev/null", O_RDWR); + if (fd > 2) + close(fd); + } while (fd >= 0 && fd <= 2); + + se = guse_lowlevel_new(&args, ci, glop, userdata); + if (se == NULL) + goto out1; + + fd = open(devname, O_RDWR); + if (fd == -1) { + if (errno == ENODEV || errno == ENOENT) + fuse_log(FUSE_LOG_ERR, "guse: device not found, try 'modprobe guse' first\n"); + else + fuse_log(FUSE_LOG_ERR, "guse: failed to open %s: %s\n", + devname, strerror(errno)); + goto err_se; + } + se->fd = fd; + + res = fuse_set_signal_handlers(se); + if (res == -1) + goto err_se; + + res = fuse_daemonize(opts.foreground); + if (res == -1) + goto err_sig; + + fuse_opt_free_args(&args); + return se; + +err_sig: + fuse_remove_signal_handlers(se); +err_se: + fuse_session_destroy(se); +out1: + free(opts.mountpoint); + fuse_opt_free_args(&args); + return NULL; +} + +void guse_lowlevel_teardown(struct fuse_session *se) +{ + fuse_remove_signal_handlers(se); + fuse_session_destroy(se); +} + +int guse_lowlevel_main(int argc, char *argv[], const struct guse_info *ci, + const struct guse_cdev_lowlevel_ops *glop, void *userdata) +{ + struct fuse_session *se; + int multithreaded; + int res; + + se = guse_lowlevel_setup(argc, argv, ci, glop, &multithreaded, + userdata); + if (se == NULL) + return 1; + + if (multithreaded) { + struct fuse_loop_config *config = fuse_loop_cfg_create(); + res = fuse_session_loop_mt(se, config); + fuse_loop_cfg_destroy(config); + } + else + res = fuse_session_loop(se); + + guse_lowlevel_teardown(se); + if (res == -1) + return 1; + + return 0; +} diff --git a/lib/meson.build b/lib/meson.build index 34dd607..961f17f 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -1,8 +1,9 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c', 'fuse_lowlevel.c', 'fuse_misc.h', 'fuse_opt.c', 'fuse_signals.c', 'buffer.c', 'cuse_lowlevel.c', - 'helper.c', 'modules/subdir.c', 'mount_util.c', - 'fuse_log.c', 'compat.c', 'util.c', 'util.h' ] + 'guse_lowlevel.c', 'helper.c', 'modules/subdir.c', + 'mount_util.c', 'fuse_log.c', 'compat.c', + 'util.c', 'util.h' ] if host_machine.system().startswith('linux') libfuse_sources += [ 'mount.c' ] -- 2.30.2