All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m26s
Test / ShareFS (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m31s
Test / Flake checks (push) Successful in 1m36s
This replaces the forking daemonise libfuse function which prevents Go callbacks from calling into the runtime. This also enforces least privilege on the daemon process. Signed-off-by: Ophestra <cat@gensokyo.uk>
519 lines
13 KiB
Go
519 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"
|
|
"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
|
|
|
|
// 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 *C.struct_fuse_args) {
|
|
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 *C.struct_fuse_args, setup *setupState) (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
|
|
}
|
|
|
|
// 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 *C.struct_fuse_args, arg string) {
|
|
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
|
}
|
|
|
|
func _main(argc int, argv **C.char) (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 := 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)
|
|
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
|
|
}
|
|
defer func() {
|
|
if opts.mountpoint != nil {
|
|
C.free(unsafe.Pointer(opts.mountpoint))
|
|
}
|
|
C.fuse_opt_free_args(&args)
|
|
}()
|
|
|
|
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) {
|
|
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
|
|
}
|