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" ) 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() { fmt.Printf("usage: %s [options] \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 || *unsafeOpts.source == 0 { 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 { // 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) { 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) } // 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), 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), }, unsafe.Pointer(&priv)) if priv.init_failed { return 1 } else { return int(fuse_main_return) } }