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; }; */ import "C" import ( "context" "encoding/gob" "errors" "fmt" "io" "log" "os" "os/exec" "os/signal" "path" "runtime" "runtime/cgo" "strconv" "syscall" "unsafe" "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/std" "hakurei.app/hst" "hakurei.app/internal/helper/proc" "hakurei.app/internal/info" "hakurei.app/message" ) type ( // closure represents a C function pointer. closure = C.closure // fuseArgs represents the fuse_args structure. fuseArgs = C.struct_fuse_args // 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 // Whether to create source directory as root. mkdir bool // Open file descriptor to fuse. Fuse int // Pathname to open for dirfd. Source *check.Absolute // 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 { log.Println("filesystem daemon must not run as root") 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(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil { log.Printf("cannot open %q: %v", setup.Source, err) goto fail } else if err = syscall.Fchdir(fd); err != nil { _ = syscall.Close(fd) log.Printf("cannot enter %q: %s", setup.Source, 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 *fuseArgs) { executableName := sharefsName if args.argc > 0 { executableName = path.Base(C.GoString(*args.argv)) } else if name, err := os.Executable(); err == nil { executableName = path.Base(name) } 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 *fuseArgs, setup *setupState, log *log.Logger) (ok bool) { var unsafeOpts struct { // Pathname to writable source directory. source *C.char // Whether to create source directory as root. mkdir C.int // 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("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1}, {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 { r := os.NewFile(uintptr(v), "setup") defer func() { if err = r.Close(); err != nil { log.Println(err) } }() if err = gob.NewDecoder(r).Decode(setup); err != nil { log.Println(err) return false } } if setup.Fuse < 3 { log.Println("invalid file descriptor", setup.Fuse) 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 } setup.mkdir = unsafeOpts.mkdir != 0 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 } // copyArgs returns a heap allocated copy of an argument slice in fuse_args representation. func copyArgs(s ...string) fuseArgs { if len(s) == 0 { return fuseArgs{argc: 0, argv: nil, allocated: 0} } 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 fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1} } // freeArgs frees the contents of argument list. func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) } // 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(s ...string) (exitCode int) { msg := message.New(log.Default()) container.TryArgv0(msg) runtime.LockOSThread() // don't mask creation mode, kernel already did that syscall.Umask(0) var pinner runtime.Pinner defer pinner.Unpin() args := copyArgs(s...) defer freeArgs(&args) // 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 } if opts.mountpoint != nil { defer C.free(unsafe.Pointer(opts.mountpoint)) } 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 } else if opts.mountpoint == nil { log.Println("no mountpoint specified") return 2 } else { // hack to keep fuse_parse_cmdline happy in the container mountpoint := C.GoString(opts.mountpoint) pathnameArg := -1 for i, arg := range s { if arg == mountpoint { pathnameArg = i break } } if pathnameArg < 0 { log.Println("mountpoint must be absolute") return 2 } s[pathnameArg] = container.Nonexistent } if !parseOpts(&args, &setup, msg.GetLogger()) { return 1 } asRoot := os.Geteuid() == 0 if asRoot { if setup.Setuid <= 0 || setup.Setgid <= 0 { log.Println("setuid and setgid must not be 0") return 1 } if setup.Fuse >= 3 { log.Println("filesystem daemon must not run as root") return 1 } if setup.mkdir { if err := os.MkdirAll(setup.Source.String(), 0700); err != nil { if !errors.Is(err, os.ErrExist) { log.Println(err) return 1 } // skip setup for existing source directory } else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil { log.Println(err) return 1 } } } else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) { log.Println("setuid and setgid has no effect when not starting as root") return 1 } else if setup.mkdir { log.Println("mkdir 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) se := C.fuse_get_session(fuse) if setup.Fuse < 3 { // unconfined, set up mount point and container if C.fuse_mount(fuse, opts.mountpoint) != 0 { return 4 } // unmounted by initial process defer func() { if exitCode == 5 { C.fuse_unmount(fuse) } }() if asRoot { if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil { log.Printf("cannot set gid: %v", err) return 5 } if err := syscall.Setgroups(nil); err != nil { log.Printf("cannot set supplementary groups: %v", err) return 5 } if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil { log.Printf("cannot set uid: %v", err) return 5 } } msg.SwapVerbose(opts.debug != 0) ctx := context.Background() if opts.foreground != 0 { c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() ctx = c } z := container.New(ctx, msg) z.AllowOrphan = opts.foreground == 0 z.Env = os.Environ() // keep fuse_parse_cmdline happy in the container z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755) if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil { log.Println(err) return 5 } else { z.Path = a } z.Args = s z.ForwardCancel = true z.SeccompPresets |= std.PresetStrict z.ParentPerm = 0700 z.Bind(setup.Source, setup.Source, std.BindWritable) if !z.AllowOrphan { z.WaitDelay = hst.WaitDelayMax z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr } z.Bind(z.Path, z.Path, 0) setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"))) var setupWriter io.WriteCloser if fd, w, err := container.Setup(&z.ExtraFiles); err != nil { log.Println(err) return 5 } else { z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd)) setupWriter = w } if err := z.Start(); err != nil { if m, ok := message.GetMessage(err); ok { log.Println(m) } else { log.Println(err) } return 5 } if err := z.Serve(); err != nil { if m, ok := message.GetMessage(err); ok { log.Println(m) } else { log.Println(err) } return 5 } if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil { log.Println(err) return 5 } else if err = setupWriter.Close(); err != nil { log.Println(err) } if !z.AllowOrphan { if err := z.Wait(); err != nil { var exitError *exec.ExitError if !errors.As(err, &exitError) || exitError == nil { log.Println(err) return 5 } switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) { case syscall.SIGINT: case syscall.SIGTERM: default: return code } } } return 0 } else { // confined C.free(unsafe.Pointer(opts.mountpoint)) // must be heap allocated opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse)) if err := os.Chdir("/"); err != nil { log.Println(err) } } if C.fuse_mount(fuse, opts.mountpoint) != 0 { return 4 } defer C.fuse_unmount(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 }