cmd/sharefs: reproduce vfs inode file attribute race
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
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>
This commit is contained in:
122
cmd/sharefs/test/raceattr.go
Normal file
122
cmd/sharefs/test/raceattr.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,6 +195,7 @@
|
|||||||
./test/interactive/vm.nix
|
./test/interactive/vm.nix
|
||||||
./test/interactive/hakurei.nix
|
./test/interactive/hakurei.nix
|
||||||
./test/interactive/trace.nix
|
./test/interactive/trace.nix
|
||||||
|
./test/interactive/raceattr.nix
|
||||||
|
|
||||||
self.nixosModules.hakurei
|
self.nixosModules.hakurei
|
||||||
home-manager.nixosModules.home-manager
|
home-manager.nixosModules.home-manager
|
||||||
|
|||||||
24
test/interactive/raceattr.nix
Normal file
24
test/interactive/raceattr.nix
Normal file
@@ -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
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user