aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/sharefs
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-07-07 18:32:56 +0900
committerOphestra <cat@gensokyo.uk>2026-07-07 18:56:27 +0900
commit6d55ee536e902d059380f79e870d06547627cc7c (patch)
treeaacf3e3ec42525cf0e7a674aa948359fd0c21ae2 /test/sharefs
parentc8e8651694415d497b0800319b2ddda361b7651f (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.nix44
-rw-r--r--test/sharefs/default.nix44
-rw-r--r--test/sharefs/raceattr.go122
-rw-r--r--test/sharefs/test.py60
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")