package main /* #cgo pkg-config: --static fuse3 #include "fuse-operations.h" #include #include extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg); extern 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 ( "encoding/gob" "fmt" "log" "os" "runtime" "runtime/cgo" "strconv" "syscall" "unsafe" "hakurei.app/container/check" "hakurei.app/internal/info" ) type ( // closure represents a C function pointer. closure = C.closure // setupState holds state used for setup. Its cgo handle is included in // sharefs_private and considered opaque to non-setup callbacks. setupState struct { // Whether sharefs_init failed. initFailed bool // Absolute pathname to open for dirfd. Source *check.Absolute // Opened by parent, ignored unless Source is nil. Dirfd int // New uid and gid to set by sharefs_init when starting as root. Setuid, Setgid int } ) func init() { gob.Register(new(setupState)) } // destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure. func destroySetup(private_data unsafe.Pointer) (ok bool) { if private_data == nil { return false } priv := (*C.struct_sharefs_private)(private_data) if h := cgo.Handle(priv.setup); h != 0 { priv.setup = 0 h.Delete() ok = true } return } //export sharefs_init func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer { ctx := C.fuse_get_context() priv := (*C.struct_sharefs_private)(ctx.private_data) setup := cgo.Handle(priv.setup).Value().(*setupState) if os.Geteuid() == 0 { if setup.Setuid <= 0 || setup.Setgid <= 0 { log.Println("setuid and setgid must not be 0") goto fail } if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); 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(setup.Setuid, setup.Setuid, setup.Setuid); 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 setup.Source == nil { priv.dirfd = C.int(setup.Dirfd) } else if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY, 0); err != nil { log.Printf("cannot open %q: %v", setup.Source.String(), err) goto fail } else if err = syscall.Fchdir(fd); err != nil { log.Printf("cannot enter %q: %s", setup.Source.String(), err) goto fail } else { priv.dirfd = C.int(fd) } return ctx.private_data fail: setup.initFailed = true C.fuse_exit(ctx.fuse) return nil } //export sharefs_destroy func sharefs_destroy(private_data unsafe.Pointer) { if private_data != nil { destroySetup(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(args *C.struct_fuse_args) { fmt.Printf("usage: %s [options] \n\n", executableName) fmt.Println("Filesystem options:") 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") fmt.Println("\nFUSE options:") C.fuse_cmdline_help() C.fuse_lib_help(args) } // parseOpts parses fuse options via fuse_opt_parse. func parseOpts(args *C.struct_fuse_args, setup *setupState) bool { 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 // Decimal string representation of open file descriptor to read setupState from. // This is an internal detail for containerisation and must not be specified directly. setup *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}, {templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0}, C._FUSE_OPT_END(), }[0], nil) == -1 { return false } 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.setup != nil { defer C.free(unsafe.Pointer(unsafeOpts.setup)) if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 { log.Println("invalid value for option setup") return false } else if err = gob.NewDecoder(os.NewFile(uintptr(v), "setup")).Decode(setup); err != nil { log.Println(err) return false } return true } if unsafeOpts.source == nil { showHelp(args) return false } else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil { log.Println(err) return false } else { setup.Source = a } if unsafeOpts.setuid == nil { setup.Setuid = -1 } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 { log.Println("invalid value for option setuid") return false } else { setup.Setuid = v } if unsafeOpts.setgid == nil { setup.Setgid = -1 } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 { log.Println("invalid value for option setgid") return false } else { setup.Setgid = v } return true } // 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 *C.struct_fuse_args, arg string) { C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg)))) } func _main(argc int, argv **C.char) int { runtime.LockOSThread() // don't mask creation mode, kernel already did that syscall.Umask(0) var pinner runtime.Pinner defer pinner.Unpin() 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") var priv C.struct_sharefs_private pinner.Pin(&priv) var setup setupState priv.setup = C.uintptr_t(cgo.NewHandle(&setup)) defer destroySetup(unsafe.Pointer(&priv)) var opts C.struct_fuse_cmdline_opts if C.fuse_parse_cmdline(&args, &opts) != 0 { return 1 } defer func() { if opts.mountpoint != nil { C.free(unsafe.Pointer(opts.mountpoint)) } C.fuse_opt_free_args(&args) }() if opts.show_version != 0 { fmt.Println("hakurei version", info.Version()) fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion())) C.fuse_lowlevel_version() return 0 } if opts.show_help != 0 { showHelp(&args) return 0 } if opts.show_help == 0 && opts.mountpoint == nil { log.Println("no mountpoint specified") return 2 } if !parseOpts(&args, &setup) { return 1 } if os.Geteuid() == 0 { if setup.Setuid <= 0 || setup.Setgid <= 0 { log.Println("setuid and setgid must not be 0") return 1 } } else if setup.Setuid > 0 || setup.Setgid > 0 { log.Println("setuid and setgid has no effect when not starting as root") return 1 } op := C.struct_fuse_operations{ init: closure(C.sharefs_init), destroy: closure(C.sharefs_destroy), // implemented in fuse-helper.c getattr: closure(C.sharefs_getattr), 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), } fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv)) if fuse == nil { return 3 } defer C.fuse_destroy(fuse) if C.fuse_mount(fuse, opts.mountpoint) != 0 { return 4 } defer C.fuse_unmount(fuse) // TODO(ophestra): spawn container here, set PR_SET_NO_NEW_PRIVS and enforce landlock if C.fuse_daemonize(opts.foreground) != 0 { return 5 } se := C.fuse_get_session(fuse) if C.fuse_set_signal_handlers(se) != 0 { return 6 } defer C.fuse_remove_signal_handlers(se) if opts.singlethread != 0 { if C.fuse_loop(fuse) != 0 { return 8 } } else { loopConfig := C.fuse_loop_cfg_create() if loopConfig == nil { return 7 } defer C.fuse_loop_cfg_destroy(loopConfig) C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd)) C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads) C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads) if C.fuse_loop_mt(fuse, loopConfig) != 0 { return 8 } } if setup.initFailed { return 1 } return 0 }