All checks were successful
Test / Create distribution (push) Successful in 44s
Test / ShareFS (push) Successful in 39s
Test / Sandbox (push) Successful in 47s
Test / Sandbox (race detector) (push) Successful in 46s
Test / Hakurei (race detector) (push) Successful in 54s
Test / Hpkg (push) Successful in 50s
Test / Hakurei (push) Successful in 55s
Test / Flake checks (push) Successful in 1m35s
This change makes it possible to check parseOpts behaviour as part of Go tests. Signed-off-by: Ophestra <cat@gensokyo.uk>
531 lines
13 KiB
Go
531 lines
13 KiB
Go
package main
|
|
|
|
/*
|
|
#cgo pkg-config: --static fuse3
|
|
|
|
#include "fuse-operations.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
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
|
|
|
|
// 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] <mountpoint>\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
|
|
|
|
// 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 {
|
|
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
|
|
}
|
|
|
|
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 os.Args {
|
|
if arg == mountpoint {
|
|
pathnameArg = i
|
|
break
|
|
}
|
|
}
|
|
if pathnameArg < 0 {
|
|
log.Println("mountpoint must be absolute")
|
|
return 2
|
|
}
|
|
os.Args[pathnameArg] = container.Nonexistent
|
|
}
|
|
|
|
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
|
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.Fuse < 3 && (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)
|
|
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 os.Geteuid() == 0 {
|
|
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
|
log.Println("setuid and setgid must not be 0")
|
|
return 5
|
|
}
|
|
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 = os.Args
|
|
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
|
|
}
|