aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/sharefs
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-12-27 03:45:09 +0900
committerOphestra <cat@gensokyo.uk>2025-12-27 04:14:00 +0900
commit2e5362e536d477b47858748a8792c64175320aa7 (patch)
treeb4627f156649fbf74decae52b97237fd1e3ee40b /cmd/sharefs
parent6d3bd27220c80eb58a11b75054fbbcf67e50cc28 (diff)
cmd/sharefs: opaque setup state
This allows unrestricted use of the type system and prepares setup code for cross-process initialisation. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/sharefs')
-rw-r--r--cmd/sharefs/fuse-operations.h7
-rw-r--r--cmd/sharefs/fuse.go166
-rw-r--r--cmd/sharefs/test/test.py9
3 files changed, 106 insertions, 76 deletions
diff --git a/cmd/sharefs/fuse-operations.h b/cmd/sharefs/fuse-operations.h
index edcfb2a7..da118c84 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 8b638b75..65b75178 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 a, err := filepath.Abs(source); err != nil {
- log.Println(err)
- return 1
- } else {
- priv.source_handle = C.uintptr_t(cgo.NewHandle(a))
- }
+ if !parseOpts(&args, &setup) {
+ return 1
+ }
- 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 cd1d8972..8be70993 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")