All checks were successful
Test / ShareFS (push) Successful in 46s
Test / Sandbox (race detector) (push) Successful in 53s
Test / Sandbox (push) Successful in 55s
Test / Hakurei (push) Successful in 1m1s
Test / Hakurei (race detector) (push) Successful in 1m0s
Test / Create distribution (push) Successful in 1m11s
Test / Flake checks (push) Successful in 1m28s
This happens in the vfs permissions check only and stale data appears to never reach userspace. Signed-off-by: Ophestra <cat@gensokyo.uk>
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
//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())
|
|
}
|
|
}
|