package main /* #cgo pkg-config: --static fuse3 #include "fuse-helper.h" #include #include 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] \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) } }