diff --git a/cmd/sharefs/test/raceattr.go b/cmd/sharefs/test/raceattr.go new file mode 100644 index 00000000..412cb2b3 --- /dev/null +++ b/cmd/sharefs/test/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()) + } +} diff --git a/flake.nix b/flake.nix index f29dc179..4de35204 100644 --- a/flake.nix +++ b/flake.nix @@ -195,6 +195,7 @@ ./test/interactive/vm.nix ./test/interactive/hakurei.nix ./test/interactive/trace.nix + ./test/interactive/raceattr.nix self.nixosModules.hakurei home-manager.nixosModules.home-manager diff --git a/test/interactive/raceattr.nix b/test/interactive/raceattr.nix new file mode 100644 index 00000000..8140beb1 --- /dev/null +++ b/test/interactive/raceattr.nix @@ -0,0 +1,24 @@ +{ lib, pkgs, ... }: +let + inherit (pkgs) buildGoModule; +in +{ + environment.systemPackages = [ + (buildGoModule rec { + name = "raceattr"; + pname = name; + tags = [ "raceattr" ]; + + src = builtins.path { + name = "${pname}-src"; + path = lib.cleanSource ../../cmd/sharefs/test; + filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path); + }; + vendorHash = null; + + preBuild = '' + go mod init hakurei.app/raceattr >& /dev/null + ''; + }) + ]; +}