diff --git a/cmd/sharefs/fuse-operations.h b/cmd/sharefs/fuse-operations.h index edcfb2a..da118c8 100644 --- a/cmd/sharefs/fuse-operations.h +++ b/cmd/sharefs/fuse-operations.h @@ -13,11 +13,8 @@ /* sharefs_private is populated by sharefs_init and contains process-wide context */ struct sharefs_private { - int dirfd; /* source dirfd opened during sharefs_init */ - bool init_failed; /* whether sharefs_init failed */ - uintptr_t source_handle; /* cgo handle of pathname to open for dirfd, freed during sharefs_init */ - uintptr_t setuid; /* uid to set by sharefs_init when running as root */ - uintptr_t setgid; /* gid to set by sharefs_init when running as root */ + int dirfd; /* source dirfd opened during sharefs_init */ + uintptr_t setup; /* cgo handle of opaque setup state */ }; int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi); diff --git a/cmd/sharefs/fuse.go b/cmd/sharefs/fuse.go index 8b638b7..65b7517 100644 --- a/cmd/sharefs/fuse.go +++ b/cmd/sharefs/fuse.go @@ -16,10 +16,11 @@ static inline int _fuse_main(int argc, char *argv[], const struct fuse_operation */ import "C" import ( + "encoding/gob" "fmt" "log" "os" - "path/filepath" + "path" "runtime" "runtime/cgo" "strconv" @@ -33,23 +34,48 @@ 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 backing directory. + Source int + // 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) - source := cgo.Handle(priv.source_handle).Value().(string) + setup := cgo.Handle(priv.setup).Value().(*setupState) - setuid, setgid := int(priv.setuid), int(priv.setgid) if os.Geteuid() == 0 { - if setuid <= 0 || setgid <= 0 { + if setup.Setuid <= 0 || setup.Setgid <= 0 { log.Println("setuid and setgid must not be 0") goto fail } - if err := syscall.Setresgid(setgid, setgid, setgid); err != nil { + if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil { log.Printf("cannot set gid: %v", err) goto fail } @@ -57,7 +83,7 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe. log.Printf("cannot set supplementary groups: %v", err) goto fail } - if err := syscall.Setresuid(setuid, setuid, setuid); err != nil { + if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil { log.Printf("cannot set uid: %v", err) goto fail } @@ -71,24 +97,12 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe. 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) - } + priv.dirfd = C.int(setup.Source) return ctx.private_data fail: - sourceHandle := cgo.Handle(priv.source_handle) - priv.source_handle = 0 - sourceHandle.Delete() - priv.init_failed = true - + setup.initFailed = true C.fuse_exit(ctx.fuse) return nil } @@ -96,12 +110,8 @@ fail: //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 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) @@ -124,11 +134,7 @@ func showHelp(args *C.struct_fuse_args) { } // parseOpts parses fuse options via fuse_opt_parse. -func parseOpts(args *C.struct_fuse_args) ( - source string, - setuid, setgid int, - ret int, -) { +func parseOpts(args *C.struct_fuse_args, setup *setupState) (ok bool) { var unsafeOpts struct { // Pathname to writable source directory. source *C.char @@ -137,6 +143,10 @@ func parseOpts(args *C.struct_fuse_args) ( 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{ @@ -144,10 +154,11 @@ func parseOpts(args *C.struct_fuse_args) ( {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 { - ret = 1 - return + return false } if unsafeOpts.source != nil { @@ -160,34 +171,59 @@ func parseOpts(args *C.struct_fuse_args) ( defer C.free(unsafe.Pointer(unsafeOpts.setgid)) } - if unsafeOpts.source == nil || *unsafeOpts.source == 0 { + 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 if err = gob.NewDecoder(os.NewFile(uintptr(v), "setup")).Decode(setup); err != nil { + log.Println(err) + return false + } + return true + } + + if unsafeOpts.source == nil { showHelp(args) - ret = 1 - return + return false + } else if source := C.GoString(unsafeOpts.source); !path.IsAbs(source) { + log.Println("source is not absolute") + return false + } else if fd, err := syscall.Open(source, syscall.O_DIRECTORY|syscall.O_RDONLY, 0); err != nil { + log.Printf("cannot open source: %v", err) + return false + } else if err = syscall.Fchdir(fd); err != nil { + _ = syscall.Close(fd) + log.Printf("cannot enter source: %s", err) + return false } else { - source = C.GoString(unsafeOpts.source) + setup.Source = fd + defer func() { + if !ok { + _ = syscall.Close(fd) + } + }() } if unsafeOpts.setuid == nil { - setuid = -1 + setup.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 + return false } else { - setuid = v + setup.Setuid = v } if unsafeOpts.setgid == nil { - setgid = -1 + setup.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 + return false } else { - setgid = v + setup.Setgid = v } - return + return true } // copyStrings returns a copy of s with null-termination. @@ -204,7 +240,7 @@ func copyStrings(s ...string) **C.char { // 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) { +func unsafeAddArgument(args *C.struct_fuse_args, arg string) { C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg)))) } @@ -225,6 +261,9 @@ func _main(argc int, argv **C.char) int { 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 { @@ -254,29 +293,18 @@ func _main(argc int, argv **C.char) int { return 2 } - { - source, setuid, setgid, ret := parseOpts(&args) - if ret != 0 { - return ret - } + if !parseOpts(&args, &setup) { + return 1 + } - 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") + if os.Geteuid() == 0 { + if setup.Setuid <= 0 || setup.Setgid <= 0 { + log.Println("setuid and setgid must not be 0") return 1 } - priv.setuid, priv.setgid = C.uintptr_t(setuid), C.uintptr_t(setgid) + } else if 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{ @@ -343,7 +371,7 @@ func _main(argc int, argv **C.char) int { } } - if priv.init_failed { + if setup.initFailed { return 1 } return 0 diff --git a/cmd/sharefs/test/test.py b/cmd/sharefs/test/test.py index cd1d897..8be7099 100644 --- a/cmd/sharefs/test/test.py +++ b/cmd/sharefs/test/test.py @@ -8,8 +8,8 @@ print(machine.succeed("/etc/sharefs -V")) machine.wait_for_unit("sharefs.service") machine.succeed("mkdir /mnt") -def check_bad_opts_output(opts, want, privileged=False): - output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"/etc/sharefs -f -o source=/proc/nonexistent,{opts} /mnt 2>&1") +def check_bad_opts_output(opts, want, source="/etc", privileged=False): + output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"/etc/sharefs -f -o source={source},{opts} /mnt 2>&1") if output != want: raise Exception(f"unexpected output: {output}") @@ -33,6 +33,11 @@ check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True) check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True) +# Bad backing directory: +check_bad_opts_output("clone_fd", "sharefs: cannot open source: no such file or directory\n", source="/proc/nonexistent") +check_bad_opts_output("clone_fd", "sharefs: cannot open source: not a directory\n", source="/proc/self/exe") +check_bad_opts_output("clone_fd", "sharefs: cannot open source: permission denied\n", source="/root") + # Make sure nothing actually got mounted: machine.fail("umount /mnt") machine.succeed("rmdir /mnt")