diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-07-07 18:32:56 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-07-07 18:56:27 +0900 |
| commit | 6d55ee536e902d059380f79e870d06547627cc7c (patch) | |
| tree | aacf3e3ec42525cf0e7a674aa948359fd0c21ae2 /test/sharefs | |
| parent | c8e8651694415d497b0800319b2ddda361b7651f (diff) | |
test: move nix files
These are too much clutter. Move them to test directory until the test suite replacement is upstreamed.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test/sharefs')
| -rw-r--r-- | test/sharefs/configuration.nix | 44 | ||||
| -rw-r--r-- | test/sharefs/default.nix | 44 | ||||
| -rw-r--r-- | test/sharefs/raceattr.go | 122 | ||||
| -rw-r--r-- | test/sharefs/test.py | 60 |
4 files changed, 270 insertions, 0 deletions
diff --git a/test/sharefs/configuration.nix b/test/sharefs/configuration.nix new file mode 100644 index 00000000..05d67dcb --- /dev/null +++ b/test/sharefs/configuration.nix @@ -0,0 +1,44 @@ +{ pkgs, ... }: +{ + users.users = { + alice = { + isNormalUser = true; + description = "Alice Foobar"; + password = "foobar"; + uid = 1000; + }; + }; + + home-manager.users.alice.home.stateVersion = "24.11"; + + # Automatically login on tty1 as a normal user: + services.getty.autologinUser = "alice"; + + environment = { + # For benchmarking sharefs: + systemPackages = [ pkgs.fsmark ]; + }; + + virtualisation = { + # Hopefully reduces spurious test failures: + memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192; + + diskSize = 6 * 1024; + + qemu.options = [ + # Increase test performance: + "-smp 16" + ]; + }; + + environment.hakurei = rec { + enable = true; + stateDir = "/var/lib/hakurei"; + sharefs.source = "${stateDir}/sdcard"; + users.alice = 0; + + extraHomeConfig = { + home.stateVersion = "23.05"; + }; + }; +} diff --git a/test/sharefs/default.nix b/test/sharefs/default.nix new file mode 100644 index 00000000..e963eaa6 --- /dev/null +++ b/test/sharefs/default.nix @@ -0,0 +1,44 @@ +{ + testers, + + system, + self, +}: +testers.nixosTest { + name = "sharefs"; + nodes.machine = + { options, pkgs, ... }: + let + fhs = + let + hakurei = options.environment.hakurei.package.default; + in + pkgs.buildFHSEnv { + pname = "hakurei-fhs"; + inherit (hakurei) version; + targetPkgs = _: hakurei.targetPkgs; + extraOutputsToInstall = [ "dev" ]; + profile = '' + export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH" + ''; + }; + in + { + environment.systemPackages = [ + # For go tests: + (pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" '' + cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei" + ${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...' + '') + ]; + + imports = [ + ./configuration.nix + + self.nixosModules.hakurei + self.inputs.home-manager.nixosModules.home-manager + ]; + }; + + testScript = builtins.readFile ./test.py; +} diff --git a/test/sharefs/raceattr.go b/test/sharefs/raceattr.go new file mode 100644 index 00000000..412cb2b3 --- /dev/null +++ b/test/sharefs/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/test/sharefs/test.py b/test/sharefs/test.py new file mode 100644 index 00000000..4b925c9c --- /dev/null +++ b/test/sharefs/test.py @@ -0,0 +1,60 @@ +start_all() +machine.wait_for_unit("multi-user.target") + +# To check sharefs version: +print(machine.succeed("sharefs -V")) + +# Make sure sharefs started: +machine.wait_for_unit("sdcard.mount") + +machine.succeed("mkdir /mnt") +def check_bad_opts_output(opts, want, source="/etc", privileged=False): + output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1") + if output != want: + raise Exception(f"unexpected output: {output}") + +# Malformed setuid/setgid representation: +check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n") +check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n") + +# Bounds check for setuid/setgid: +check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n") +check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n") +check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n") +check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n") + +# Non-root setuid/setgid: +check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") +check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") +check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") +check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n") + +# Starting as root without setuid/setgid: +check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True) +check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True) +check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True) + +# Make sure nothing actually got mounted: +machine.fail("umount /mnt") +machine.succeed("rmdir /mnt") + +# Unprivileged mount/unmount: +machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}") +machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard") +machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check") +machine.succeed("sudo -u alice -i umount /home/alice/sdcard") +machine.succeed("sudo -u alice -i rm /home/alice/persistent/check") +machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}") + +# Benchmark sharefs: +machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt") +machine.copy_from_vm("/tmp/fs_log.txt", "") + +# Check permissions: +machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check") +machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check") +machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark") +machine.fail("ls /var/lib/hakurei/sdcard/fs_mark") + +# Run hakurei tests on sharefs: +machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests") |
