All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m30s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m36s
Test / ShareFS (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m30s
This optional behaviour is required on NixOS as it is otherwise impossible to set this up: systemd.mounts breaks startup order somehow even though my unit looks identical to generated ones, fileSystems does not support any kind of initialisation or ordering other than against other mount points. Signed-off-by: Ophestra <cat@gensokyo.uk>
556 lines
14 KiB
Go
556 lines
14 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
|
|
|
|
// 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] <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
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
} 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 os.Geteuid() == 0 {
|
|
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
|
log.Println("setuid and setgid must not be 0")
|
|
return 5
|
|
}
|
|
|
|
if setup.mkdir {
|
|
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
|
if !errors.Is(err, os.ErrExist) {
|
|
log.Println(err)
|
|
return 5
|
|
}
|
|
// skip setup for existing source directory
|
|
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
|
log.Println(err)
|
|
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 = 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
|
|
}
|