diff --git a/cmd/sharefs/fuse.go b/cmd/sharefs/fuse.go index ea3d928..9517b56 100644 --- a/cmd/sharefs/fuse.go +++ b/cmd/sharefs/fuse.go @@ -24,6 +24,7 @@ import ( "os" "os/exec" "os/signal" + "path" "runtime" "runtime/cgo" "strconv" @@ -43,6 +44,9 @@ 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 { @@ -127,7 +131,14 @@ func sharefs_destroy(private_data unsafe.Pointer) { } // showHelp prints the help message. -func showHelp(args *C.struct_fuse_args) { +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] \n\n", executableName) fmt.Println("Filesystem options:") @@ -141,7 +152,7 @@ func showHelp(args *C.struct_fuse_args) { } // parseOpts parses fuse options via fuse_opt_parse. -func parseOpts(args *C.struct_fuse_args, setup *setupState) (ok bool) { +func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) { var unsafeOpts struct { // Pathname to writable source directory. source *C.char @@ -233,25 +244,28 @@ func parseOpts(args *C.struct_fuse_args, setup *setupState) (ok bool) { return true } -// copyStrings returns a copy of s with null-termination. -func copyStrings(s ...string) **C.char { +// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation. +func copyArgs(s ...string) fuseArgs { if len(s) == 0 { - return nil + 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 &args[0] + 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 *C.struct_fuse_args, arg string) { +func unsafeAddArgument(args *fuseArgs, arg string) { C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg)))) } -func _main(argc int, argv **C.char) (exitCode int) { +func _main(s ...string) (exitCode int) { msg := message.New(log.Default()) container.TryArgv0(msg) runtime.LockOSThread() @@ -262,7 +276,8 @@ func _main(argc int, argv **C.char) (exitCode int) { var pinner runtime.Pinner defer pinner.Unpin() - args := C.struct_fuse_args{argc: C.int(argc), argv: argv, allocated: 1} + args := copyArgs(s...) + defer freeArgs(&args) // this causes the kernel to enforce access control based on // struct stat populated by sharefs_getattr @@ -278,12 +293,9 @@ func _main(argc int, argv **C.char) (exitCode int) { 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.mountpoint != nil { + defer C.free(unsafe.Pointer(opts.mountpoint)) + } if opts.show_version != 0 { fmt.Println("hakurei version", info.Version()) @@ -315,7 +327,7 @@ func _main(argc int, argv **C.char) (exitCode int) { os.Args[pathnameArg] = container.Nonexistent } - if !parseOpts(&args, &setup) { + if !parseOpts(&args, &setup, msg.GetLogger()) { return 1 } diff --git a/cmd/sharefs/fuse_test.go b/cmd/sharefs/fuse_test.go new file mode 100644 index 0000000..d25bc88 --- /dev/null +++ b/cmd/sharefs/fuse_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "bytes" + "log" + "reflect" + "testing" + + "hakurei.app/container/check" +) + +func TestParseOpts(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + args []string + want setupState + wantLog string + wantOk bool + }{ + {"zero length", []string{}, setupState{}, "", false}, + + {"not absolute", []string{"sharefs", + "-o", "source=nonexistent", + "-o", "setuid=1023", + "-o", "setgid=1023", + }, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false}, + + {"not specified", []string{"sharefs", + "-o", "setuid=1023", + "-o", "setgid=1023", + }, setupState{}, "", false}, + + {"invalid setuid", []string{"sharefs", + "-o", "source=/proc/nonexistent", + "-o", "setuid=ff", + "-o", "setgid=1023", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + }, "sharefs: invalid value for option setuid\n", false}, + + {"invalid setgid", []string{"sharefs", + "-o", "source=/proc/nonexistent", + "-o", "setuid=1023", + "-o", "setgid=ff", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + Setuid: 1023, + }, "sharefs: invalid value for option setgid\n", false}, + + {"simple", []string{"sharefs", + "-o", "source=/proc/nonexistent", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + Setuid: -1, + Setgid: -1, + }, "", true}, + + {"root", []string{"sharefs", + "-o", "source=/proc/nonexistent", + "-o", "setuid=1023", + "-o", "setgid=1023", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + Setuid: 1023, + Setgid: 1023, + }, "", true}, + + {"setuid", []string{"sharefs", + "-o", "source=/proc/nonexistent", + "-o", "setuid=1023", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + Setuid: 1023, + Setgid: -1, + }, "", true}, + + {"setgid", []string{"sharefs", + "-o", "source=/proc/nonexistent", + "-o", "setgid=1023", + }, setupState{ + Source: check.MustAbs("/proc/nonexistent"), + Setuid: -1, + Setgid: 1023, + }, "", true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var ( + got setupState + buf bytes.Buffer + ) + args := copyArgs(tc.args...) + defer freeArgs(&args) + unsafeAddArgument(&args, "-odefault_permissions\x00") + + if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk { + t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk) + } + + if !reflect.DeepEqual(&got, &tc.want) { + t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want) + } + + if buf.String() != tc.wantLog { + t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog) + } + }) + } +} diff --git a/cmd/sharefs/main.go b/cmd/sharefs/main.go index c90c862..941a166 100644 --- a/cmd/sharefs/main.go +++ b/cmd/sharefs/main.go @@ -3,23 +3,14 @@ package main import ( "log" "os" - "path" ) -// executableName is the [path.Base] name of the executable that started the current process. -var executableName = func() string { - if len(os.Args) > 0 { - return path.Base(os.Args[0]) - } else if name, err := os.Executable(); err != nil { - return "sharefs" - } else { - return path.Base(name) - } -}() +// sharefsName is the prefix used by log.std in the sharefs process. +const sharefsName = "sharefs" func main() { log.SetFlags(0) - log.SetPrefix(executableName + ": ") + log.SetPrefix(sharefsName + ": ") - os.Exit(_main(len(os.Args), copyStrings(os.Args...))) + os.Exit(_main(os.Args...)) }