aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/sharefs/fuse.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-12-25 04:05:54 +0900
committerOphestra <cat@gensokyo.uk>2025-12-25 05:13:02 +0900
commit7bfbd5981025282007f09225e22e895523d7a7bf (patch)
tree14e4ea7475ab63c8fa22845d287a307fb029c110 /cmd/sharefs/fuse.go
parentea815a59e85a5acad8e22e98a16cdc6de1323824 (diff)
cmd/sharefs: implement shared filesystem
This is for passing files between applications, similar to android /sdcard. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/sharefs/fuse.go')
-rw-r--r--cmd/sharefs/fuse.go265
1 files changed, 265 insertions, 0 deletions
diff --git a/cmd/sharefs/fuse.go b/cmd/sharefs/fuse.go
new file mode 100644
index 00000000..ac276dd7
--- /dev/null
+++ b/cmd/sharefs/fuse.go
@@ -0,0 +1,265 @@
+package main
+
+/*
+#cgo pkg-config: --static fuse3
+
+#include "fuse-helper.h"
+#include <stdlib.h>
+#include <string.h>
+
+extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
+void sharefs_destroy(void *private_data);
+
+typedef void (*closure)();
+static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
+static inline int _fuse_main(int argc, char *argv[], const struct fuse_operations *op, void *user_data) { return fuse_main(argc, argv, op, user_data); }
+*/
+import "C"
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "syscall"
+ "unsafe"
+)
+
+type (
+ // closure represents a C function pointer.
+ closure = C.closure
+
+ // fuseArgs represents the fuse_args structure.
+ fuseArgs = C.struct_fuse_args
+)
+
+var (
+ // initFailed is set to true by sharefs_init if initialisation was unsuccessful.
+ initFailed bool
+
+ // initSource is pathname to the writable source directory used by sharefs_init to populate sharefs_private.
+ initSource string
+ // initSetUidGid is the uid and gid to set by sharefs_init when running as root.
+ initSetUidGid [2]int
+)
+
+//export sharefs_init
+func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
+ private_data := C.malloc(C.size_t(unsafe.Sizeof(C.struct_sharefs_private{})))
+ priv := (*C.struct_sharefs_private)(private_data)
+
+ if os.Geteuid() == 0 {
+ if initSetUidGid[0] <= 0 || initSetUidGid[1] <= 0 {
+ log.Println("setuid and setgid must not be 0")
+ goto fail
+ }
+ if err := syscall.Setresgid(initSetUidGid[1], initSetUidGid[1], initSetUidGid[1]); err != nil {
+ log.Printf("cannot set gid: %v", err)
+ goto fail
+ }
+ if err := syscall.Setgroups(nil); err != nil {
+ log.Printf("cannot set supplementary groups: %v", err)
+ goto fail
+ }
+ if err := syscall.Setresuid(initSetUidGid[0], initSetUidGid[0], initSetUidGid[0]); err != nil {
+ log.Printf("cannot set uid: %v", err)
+ goto fail
+ }
+ }
+
+ cfg.use_ino = C.true
+ cfg.direct_io = C.false
+ // getattr is context-dependent
+ cfg.attr_timeout = 0
+ cfg.entry_timeout = 0
+ cfg.negative_timeout = 0
+
+ // all future filesystem operations happen through this dirfd
+ if fd, err := syscall.Open(initSource, syscall.O_DIRECTORY|syscall.O_RDONLY, 0); err != nil {
+ log.Printf("cannot open %q: %v", initSource, err)
+ goto fail
+ } else if err = syscall.Fchdir(fd); err != nil {
+ log.Printf("cannot enter %q: %s", initSource, err)
+ goto fail
+ } else {
+ priv.dirfd = C.int(fd)
+ }
+
+ return private_data
+
+fail:
+ C.free(private_data)
+ C.fuse_exit(C.fuse_get_context().fuse)
+ initFailed = true
+ return nil
+}
+
+//export sharefs_destroy
+func sharefs_destroy(private_data unsafe.Pointer) {
+ if private_data != nil {
+ defer C.free(private_data)
+ priv := (*C.struct_sharefs_private)(private_data)
+
+ if err := syscall.Close(int(priv.dirfd)); err != nil {
+ log.Printf("cannot close source directory: %v", err)
+ }
+ }
+}
+
+// showHelp prints the help message.
+func showHelp() {
+ fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
+ C.fuse_cmdline_help()
+ C.fuse_lowlevel_help()
+ fmt.Println(" -o source=/data/media source directory to be mounted")
+ fmt.Println(" -o setuid=1023 uid to run as when starting as root")
+ fmt.Println(" -o setgid=1023 gid to run as when starting as root")
+}
+
+// parseOpts parses fuse options via fuse_opt_parse.
+func parseOpts(args *C.struct_fuse_args) (
+ source string,
+ setuid, setgid int,
+ ret int,
+) {
+ var unsafeOpts struct {
+ // Pathname to writable source directory.
+ source *C.char
+
+ // Decimal string representation of uid to set when running as root.
+ setuid *C.char
+ // Decimal string representation of gid to set when running as root.
+ setgid *C.char
+ }
+
+ if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
+ {templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
+ {templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
+ {templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
+
+ C._FUSE_OPT_END(),
+ }[0], nil) == -1 {
+ ret = 1
+ return
+ }
+
+ if unsafeOpts.source != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.source))
+ }
+ if unsafeOpts.setuid != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.setuid))
+ }
+ if unsafeOpts.setgid != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.setgid))
+ }
+
+ if unsafeOpts.source == nil {
+ showHelp()
+ ret = 1
+ return
+ } else {
+ source = C.GoString(unsafeOpts.source)
+ }
+
+ if unsafeOpts.setuid == nil {
+ setuid = -1
+ } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
+ log.Println("invalid value for option setuid")
+ ret = 1
+ return
+ } else {
+ setuid = v
+ }
+ if unsafeOpts.setgid == nil {
+ setgid = -1
+ } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
+ log.Println("invalid value for option setgid")
+ ret = 1
+ return
+ } else {
+ setgid = v
+ }
+
+ return
+}
+
+// copyStrings returns a copy of s with null-termination.
+func copyStrings(s ...string) **C.char {
+ if len(s) == 0 {
+ return nil
+ }
+ args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
+ for i, arg := range s {
+ args[i] = C.CString(arg)
+ }
+ return &args[0]
+}
+
+// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
+// The last byte of arg must be 0.
+func unsafeAddArgument(args *fuseArgs, arg string) {
+ C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
+}
+
+func _main(argc int, argv **C.char) int {
+ args := C.struct_fuse_args{argc: C.int(argc), argv: argv, allocated: 1}
+
+ // this causes the kernel to enforce access control based on
+ // struct stat populated by sharefs_getattr
+ unsafeAddArgument(&args, "-odefault_permissions\x00")
+
+ {
+ source, setuid, setgid, ret := parseOpts(&args)
+ if ret != 0 {
+ return ret
+ }
+
+ if a, err := filepath.Abs(source); err != nil {
+ log.Println(err)
+ return 1
+ } else {
+ initSource = a
+ }
+
+ if os.Geteuid() == 0 {
+ if setuid <= 0 || setgid <= 0 {
+ log.Println("setuid and setgid must not be 0")
+ return 1
+ }
+ } else if setuid > 0 || setgid > 0 {
+ log.Println("setuid and setgid has no effect when not starting as root")
+ return 1
+ }
+ initSetUidGid[0], initSetUidGid[1] = setuid, setgid
+ }
+
+ // TODO(ophestra): spawn container here, set PR_SET_NO_NEW_PRIVS and enforce landlock
+ fuse_main_return := C._fuse_main(args.argc, args.argv, &C.struct_fuse_operations{
+ init: closure(C.sharefs_init),
+ destroy: closure(C.sharefs_destroy),
+
+ // implemented in fuse-helper.c
+ getattr: closure(C.sharefs_getattr),
+ readlink: closure(C.sharefs_readlink),
+ readdir: closure(C.sharefs_readdir),
+ mkdir: closure(C.sharefs_mkdir),
+ unlink: closure(C.sharefs_unlink),
+ rmdir: closure(C.sharefs_rmdir),
+ rename: closure(C.sharefs_rename),
+ truncate: closure(C.sharefs_truncate),
+ utimens: closure(C.sharefs_utimens),
+ create: closure(C.sharefs_create),
+ open: closure(C.sharefs_open),
+ read: closure(C.sharefs_read),
+ write: closure(C.sharefs_write),
+ statfs: closure(C.sharefs_statfs),
+ release: closure(C.sharefs_release),
+ fsync: closure(C.sharefs_fsync),
+ }, nil)
+
+ if initFailed {
+ return 1
+ } else {
+ return int(fuse_main_return)
+ }
+}