aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/sharefs
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-04-08 15:39:04 +0900
committerOphestra <cat@gensokyo.uk>2026-04-08 15:51:36 +0900
commit351d6c5a35adcf07ccfe62add266d5db540400ed (patch)
tree5ac03f17b24dc05f2874bf224fd7e94c26d5b6e2 /cmd/sharefs
parentf23f73701c4ccf96ab4a1c28d4ca163f8e96f2f4 (diff)
cmd/sharefs: reproduce vfs inode file attribute race
This happens in the vfs permissions check only and stale data appears to never reach userspace. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/sharefs')
-rw-r--r--cmd/sharefs/test/raceattr.go122
1 files changed, 122 insertions, 0 deletions
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())
+ }
+}