From a9e2749f6654d0aa07b274a45c9177d10323f80a Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 6 Oct 2026 22:23:20 +0900 Subject: internal/testsuite: move from test This structure is a lot less clumsy than the old nix-centric layout. Signed-off-by: Ophestra --- cmd/sharefs/testsuite/main.go | 119 +++++++++++++++++++++++++++++++++++++ cmd/sharefs/testsuite/raceattr.go | 122 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 cmd/sharefs/testsuite/main.go create mode 100644 cmd/sharefs/testsuite/raceattr.go (limited to 'cmd/sharefs/testsuite') diff --git a/cmd/sharefs/testsuite/main.go b/cmd/sharefs/testsuite/main.go new file mode 100644 index 00000000..167875a1 --- /dev/null +++ b/cmd/sharefs/testsuite/main.go @@ -0,0 +1,119 @@ +//go:build testsuite + +// The sharefs test program checks cli behaviour and exercises the filesystem +// implemented by cmd/sharefs using fs_mark. +package main + +import ( + "errors" + "log" + "os" + "os/exec" + "strings" + "syscall" + + "hakurei.app/internal/testsuite" +) + +// checkBadOpts invokes cmd/sharefs with the specified options and compares +// the resulting error message. +func checkBadOpts(cred *syscall.Credential, opts, want string) { + var buf strings.Builder + buf.Grow(len(want)) + + cmd := exec.Command( + "sharefs", + "-f", + "-o", "source=/etc,"+opts, + "/mnt", + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + cmd.Stderr = &buf + err := cmd.Run() + if err == nil { + log.Fatalf("opts=%q, unexpected success", opts) + } + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if !e.Exited() { + log.Fatal(e) + } + + if got := buf.String(); got != want { + log.Fatalf("opts=%q\n\t got:%q\n\twant:%q", opts, got, want) + } +} + +func main() { + go testsuite.ReceiveSignals() + + cred := syscall.Credential{Uid: 1000, Gid: 100} + if err := os.Mkdir("result", 0755); err != nil { + log.Fatal(err) + } + + done := make(chan struct{}) + go func() { + defer close(done) + + testsuite.MustRun( + nil, nil, + "fs_mark", + "-v", + "-d", "/sdcard/fs_mark", + "-l", "result/fs_mark.log", + ) + }() + + log.Println("checking malformed setuid/setgid representation") + checkBadOpts(&cred, "setuid=ff", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=ff", "sharefs: invalid value for option setgid\n") + + log.Println("checking bounds check for setuid/setgid") + checkBadOpts(&cred, "setuid=0", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=0", "sharefs: invalid value for option setgid\n") + checkBadOpts(&cred, "setuid=-1", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=-1", "sharefs: invalid value for option setgid\n") + + log.Println("checking non-root setuid/setgid") + checkBadOpts(&cred, "setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "mkdir", "sharefs: mkdir has no effect when not starting as root\n") + + log.Println("checking root without setuid/setgid") + checkBadOpts(nil, "allow_other", "sharefs: setuid and setgid must not be 0\n") + checkBadOpts(nil, "setuid=1023", "sharefs: setuid and setgid must not be 0\n") + checkBadOpts(nil, "setgid=1023", "sharefs: setuid and setgid must not be 0\n") + + log.Println("verifying mount point") + if err := os.Remove("/mnt"); err != nil { + log.Fatal(err) + } + + log.Println("checking unprivileged mount/unmount") + testsuite.MustRun(&cred, nil, "mkdir", "/tmp/sdcard", "/tmp/persistent") + testsuite.MustRun(&cred, nil, "sharefs", "-o", "source=/tmp/persistent", "/tmp/sdcard") + testsuite.MustRun(&cred, nil, "touch", "/tmp/sdcard/check") + testsuite.MustRun(&cred, nil, "umount", "/tmp/sdcard") + testsuite.MustRun(&cred, nil, "rm", "/tmp/persistent/check") + testsuite.MustRun(&cred, nil, "rmdir", "/tmp/sdcard", "/tmp/persistent") + + log.Println("waiting for fs_mark to complete") + <-done + + const backingDir = "/var/lib/sdcard" + sharefsCred := syscall.Credential{Uid: 1023, Gid: 1023} + log.Println("checking permissions") + testsuite.MustRun(&sharefsCred, nil, "touch", backingDir+"/fs_mark/.check") + testsuite.MustRun(&sharefsCred, nil, "rm", backingDir+"/fs_mark/.check") + testsuite.MustRun(&cred, nil, "rm", "-rf", "/sdcard/fs_mark") + if _, err := os.ReadDir(backingDir + "/fs_mark"); err == nil { + log.Fatal("fs_mark directory was not removed") + } else if !errors.Is(err, os.ErrNotExist) { + log.Fatal(err) + } +} diff --git a/cmd/sharefs/testsuite/raceattr.go b/cmd/sharefs/testsuite/raceattr.go new file mode 100644 index 00000000..412cb2b3 --- /dev/null +++ b/cmd/sharefs/testsuite/raceattr.go @@ -0,0 +1,122 @@ +//go:build raceattr + +// The raceattr program reproduces vfs inode file attribute race. +// +// Even though libfuse high-level API presents the address of a struct stat +// alongside struct fuse_context, file attributes are actually inherent to the +// inode, instead of the specific call from userspace. The kernel implementation +// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous +// call) impossible or very unlikely to reach userspace via the stat family of +// syscalls. However, when using default_permissions to have the VFS check +// permissions, this race still happens, despite the resulting struct stat being +// correct when overriding the check via capabilities otherwise. +// +// This program reproduces the failure, but because of its continuous nature, it +// is provided independent of the vm integration test suite. +package main + +import ( + "context" + "flag" + "log" + "os" + "os/signal" + "runtime" + "sync" + "sync/atomic" + "syscall" +) + +func newStatAs( + ctx context.Context, cancel context.CancelFunc, + n *atomic.Uint64, ok *atomic.Bool, + uid uint32, pathname string, + continuous bool, +) func() { + return func() { + runtime.LockOSThread() + defer cancel() + + if _, _, errno := syscall.Syscall( + syscall.SYS_SETUID, uintptr(uid), + 0, 0, + ); errno != 0 { + cancel() + log.Printf("cannot set uid to %d: %s", uid, errno) + } + + var stat syscall.Stat_t + for { + if ctx.Err() != nil { + return + } + + if err := syscall.Lstat(pathname, &stat); err != nil { + // SHAREFS_PERM_DIR not world executable, or + // SHAREFS_PERM_REG not world readable + if !continuous { + cancel() + } + ok.Store(true) + log.Printf("uid %d: %v", uid, err) + } else if stat.Uid != uid { + // appears to be unreachable + if !continuous { + cancel() + } + ok.Store(true) + log.Printf("got uid %d instead of %d", stat.Uid, uid) + } + n.Add(1) + } + } +} + +func main() { + log.SetFlags(0) + log.SetPrefix("raceattr: ") + + p := flag.String("target", "/sdcard/raceattr", "pathname of test file") + u0 := flag.Int("uid0", 1<<10-1, "first uid") + u1 := flag.Int("uid1", 1<<10-2, "second uid") + count := flag.Int("count", 1, "threads per uid") + continuous := flag.Bool("continuous", false, "keep running even after reproduce") + flag.Parse() + + if os.Geteuid() != 0 { + log.Fatal("this program must run as root") + } + + ctx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGHUP, + ) + + if err := os.WriteFile(*p, nil, 0); err != nil { + log.Fatal(err) + } + + var ( + wg sync.WaitGroup + + n atomic.Uint64 + ok atomic.Bool + ) + + if *count < 1 { + *count = 1 + } + for range *count { + wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous)) + if *u1 >= 0 { + wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous)) + } + } + + wg.Wait() + if !*continuous && ok.Load() { + log.Printf("reproduced after %d calls", n.Load()) + } +} -- cgit v1.3.1