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 ( "fmt" "log" "os" "path/filepath" "runtime" "runtime/cgo" "strconv" "syscall" "unsafe" "hakurei.app/internal/info" ) type ( // closure represents a C function pointer. closure = C.closure // fuseArgs represents the fuse_args structure. fuseArgs = C.struct_fuse_args ) //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) source := cgo.Handle(priv.source_handle).Value().(string) setuid, setgid := int(priv.setuid), int(priv.setgid) if os.Geteuid() == 0 { if setuid <= 0 || setgid <= 0 { log.Println("setuid and setgid must not be 0") goto fail } if err := syscall.Setresgid(setgid, setgid, 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(setuid, setuid, 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 fd, err := syscall.Open(source, syscall.O_DIRECTORY|syscall.O_RDONLY, 0); err != nil { log.Printf("cannot open %q: %v", source, err) goto fail } else if err = syscall.Fchdir(fd); err != nil { log.Printf("cannot enter %q: %s", source, err) goto fail } else { priv.dirfd = C.int(fd) } return ctx.private_data fail: sourceHandle := cgo.Handle(priv.source_handle) priv.source_handle = 0 sourceHandle.Delete() priv.init_failed = true C.fuse_exit(ctx.fuse) return nil } //export sharefs_destroy func sharefs_destroy(private_data unsafe.Pointer) { if private_data != nil { priv := (*C.struct_sharefs_private)(private_data) if priv.source_handle != 0 { sourceHandle := cgo.Handle(priv.source_handle) priv.source_handle = 0 sourceHandle.Delete() } 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) ( 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 || *unsafeOpts.source == 0 { showHelp(args) 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 { 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 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 } { 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 { priv.source_handle = C.uintptr_t(cgo.NewHandle(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 } priv.setuid, priv.setgid = C.uintptr_t(setuid), C.uintptr_t(setgid) } 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 priv.init_failed { return 1 } return 0 }