From 6d55ee536e902d059380f79e870d06547627cc7c Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 7 Jul 2026 18:32:56 +0900 Subject: 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 --- .gitea/workflows/release.yml | 2 +- .gitea/workflows/test.yml | 18 +- README.md | 7 +- cmd/hsu/package.nix | 23 - cmd/sharefs/test/configuration.nix | 44 -- cmd/sharefs/test/default.nix | 44 -- cmd/sharefs/test/raceattr.go | 122 ---- cmd/sharefs/test/test.py | 60 -- flake.lock | 49 -- flake.nix | 255 -------- nixos.nix | 407 ------------- options.md | 1175 ------------------------------------ options.nix | 364 ----------- package.nix | 144 ----- test/default.nix | 2 +- test/flake.lock | 49 ++ test/flake.nix | 255 ++++++++ test/hsu.nix | 23 + test/interactive/raceattr.nix | 2 +- test/nixos.nix | 407 +++++++++++++ test/options.md | 1175 ++++++++++++++++++++++++++++++++++++ test/options.nix | 364 +++++++++++ test/package.nix | 144 +++++ test/sandbox/default.nix | 2 +- test/sharefs/configuration.nix | 44 ++ test/sharefs/default.nix | 44 ++ test/sharefs/raceattr.go | 122 ++++ test/sharefs/test.py | 60 ++ 28 files changed, 2701 insertions(+), 2706 deletions(-) delete mode 100644 cmd/hsu/package.nix delete mode 100644 cmd/sharefs/test/configuration.nix delete mode 100644 cmd/sharefs/test/default.nix delete mode 100644 cmd/sharefs/test/raceattr.go delete mode 100644 cmd/sharefs/test/test.py delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 nixos.nix delete mode 100644 options.md delete mode 100644 options.nix delete mode 100644 package.nix create mode 100644 test/flake.lock create mode 100644 test/flake.nix create mode 100644 test/hsu.nix create mode 100644 test/nixos.nix create mode 100644 test/options.md create mode 100644 test/options.nix create mode 100644 test/package.nix create mode 100644 test/sharefs/configuration.nix create mode 100644 test/sharefs/default.nix create mode 100644 test/sharefs/raceattr.go create mode 100644 test/sharefs/test.py diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ac263704..134d8bd1 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v4 - name: Build for release - run: nix build --print-out-paths --print-build-logs .#dist + run: nix build --print-out-paths --print-build-logs ./test#dist - name: Release uses: https://gitea.com/actions/release-action@main diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 7066d0d4..d1fc7ef8 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v4 - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei + run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.hakurei - name: Upload test output uses: actions/upload-artifact@v3 @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v4 - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race + run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.race - name: Upload test output uses: actions/upload-artifact@v3 @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@v4 - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox + run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.sandbox - name: Upload test output uses: actions/upload-artifact@v3 @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@v4 - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race + run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.sandbox-race - name: Upload test output uses: actions/upload-artifact@v3 @@ -80,7 +80,7 @@ jobs: uses: actions/checkout@v4 - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs + run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.sharefs - name: Upload test output uses: actions/upload-artifact@v3 @@ -103,7 +103,7 @@ jobs: uses: actions/checkout@v4 - name: Run checks - run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check + run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check ./test dist: name: Create distribution @@ -116,9 +116,9 @@ jobs: id: build-test run: >- export HAKUREI_REV="$(git rev-parse --short HEAD)" && - sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix && - nix build --print-out-paths --print-build-logs .#dist && - mv package.nix.old package.nix && + sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' test/package.nix && + nix build --print-out-paths --print-build-logs ./test#dist && + mv test/{package.nix.old,package.nix} && echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT - name: Upload test build diff --git a/README.md b/README.md index 7375ba0b..15f4b095 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,4 @@ package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa). - [fuse](https://github.com/libfuse/libfuse) to implement the filesystem. New dependencies will generally not be added. Patches adding new dependencies -are very likely to be rejected. - -## NixOS Module (deprecated) - -The NixOS module is in maintenance mode and will be removed once planterette is -feature-complete. Full module documentation can be found [here](options.md). +are very likely to be rejected. \ No newline at end of file diff --git a/cmd/hsu/package.nix b/cmd/hsu/package.nix deleted file mode 100644 index 6075cdb1..00000000 --- a/cmd/hsu/package.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - lib, - buildGoModule, - hakurei ? abort "hakurei package required", -}: - -buildGoModule { - pname = "${hakurei.pname}-hsu"; - inherit (hakurei) version; - - src = ./.; - inherit (hakurei) vendorHash; - env.CGO_ENABLED = 0; - - preBuild = '' - go mod init hsu >& /dev/null - ''; - - ldflags = lib.attrsets.foldlAttrs ( - ldflags: name: value: - ldflags ++ [ "-X main.${name}=${value}" ] - ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; }; -} diff --git a/cmd/sharefs/test/configuration.nix b/cmd/sharefs/test/configuration.nix deleted file mode 100644 index 05d67dcb..00000000 --- a/cmd/sharefs/test/configuration.nix +++ /dev/null @@ -1,44 +0,0 @@ -{ 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/cmd/sharefs/test/default.nix b/cmd/sharefs/test/default.nix deleted file mode 100644 index e963eaa6..00000000 --- a/cmd/sharefs/test/default.nix +++ /dev/null @@ -1,44 +0,0 @@ -{ - 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/cmd/sharefs/test/raceattr.go b/cmd/sharefs/test/raceattr.go deleted file mode 100644 index 412cb2b3..00000000 --- a/cmd/sharefs/test/raceattr.go +++ /dev/null @@ -1,122 +0,0 @@ -//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/cmd/sharefs/test/test.py b/cmd/sharefs/test/test.py deleted file mode 100644 index 4b925c9c..00000000 --- a/cmd/sharefs/test/test.py +++ /dev/null @@ -1,60 +0,0 @@ -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") diff --git a/flake.lock b/flake.lock deleted file mode 100644 index ce9fe45f..00000000 --- a/flake.lock +++ /dev/null @@ -1,49 +0,0 @@ -{ - "nodes": { - "home-manager": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1780361225, - "narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "e28654b71096e08c019d4861ca26acb646f583d8", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "release-26.05", - "repo": "home-manager", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1780453794, - "narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "home-manager": "home-manager", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 92548b40..00000000 --- a/flake.nix +++ /dev/null @@ -1,255 +0,0 @@ -{ - description = "hakurei container tool and nixos module"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; - - home-manager = { - url = "github:nix-community/home-manager/release-26.05"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; - - outputs = - { - self, - nixpkgs, - home-manager, - }: - let - supportedSystems = [ - "aarch64-linux" - "i686-linux" - "x86_64-linux" - ]; - - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); - in - { - nixosModules.hakurei = import ./nixos.nix self.packages; - - checks = forAllSystems ( - system: - let - pkgs = nixpkgsFor.${system}; - - inherit (pkgs) - runCommandLocal - callPackage - nixfmt - deadnix - statix - ; - in - { - hakurei = callPackage ./test { inherit system self; }; - race = callPackage ./test { - inherit system self; - withRace = true; - }; - - sandbox = callPackage ./test/sandbox { inherit self; }; - sandbox-race = callPackage ./test/sandbox { - inherit self; - withRace = true; - }; - - sharefs = callPackage ./cmd/sharefs/test { inherit system self; }; - - formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } '' - cd ${./.} - - echo "running nixfmt..." - nixfmt --width=256 --check . - - touch $out - ''; - - lint = - runCommandLocal "check-lint" - { - nativeBuildInputs = [ - deadnix - statix - ]; - } - '' - cd ${./.} - - echo "running deadnix..." - deadnix --fail - - echo "running statix..." - statix check . - - touch $out - ''; - } - ); - - packages = forAllSystems ( - system: - let - inherit (self.packages.${system}) hakurei hsu; - pkgs = nixpkgsFor.${system}; - in - { - default = hakurei; - hakurei = pkgs.pkgsStatic.callPackage ./package.nix { - inherit (pkgs) - # passthru.buildInputs - go_1_26 - clang - - # nativeBuildInputs - pkg-config - wayland-scanner - makeBinaryWrapper - - # appPackages - glibc - xdg-dbus-proxy - - # for check - util-linux - nettools - ; - }; - hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; }; - sharefs = pkgs.linkFarm "sharefs" { - "bin/sharefs" = "${hakurei}/libexec/sharefs"; - "bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs"; - }; - - dist = - pkgs.runCommand "${hakurei.name}-dist" - { - buildInputs = hakurei.targetPkgs ++ [ - pkgs.pkgsStatic.musl - ]; - } - '' - cd $(mktemp -d) \ - && cp -r ${hakurei.src}/. . \ - && chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \ - && chmod -R +w . - - CC="musl-clang -O3 -Werror -Qunused-arguments" \ - GOCACHE="$(mktemp -d)" \ - PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ - DESTDIR="$out" \ - ./all.sh - ''; - } - ); - - devShells = forAllSystems ( - system: - let - inherit (self.packages.${system}) hakurei; - pkgs = nixpkgsFor.${system}; - in - { - default = pkgs.mkShell { - buildInputs = hakurei.targetPkgs; - hardeningDisable = [ "fortify" ]; - }; - withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; - - vm = - let - nixos = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - { - environment = { - systemPackages = [ - (pkgs.buildFHSEnv { - pname = "hakurei-fhs"; - inherit (hakurei) version; - targetPkgs = _: hakurei.targetPkgs; - extraOutputsToInstall = [ "dev" ]; - profile = '' - export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH" - ''; - }) - ]; - - hakurei = - let - # this is used for interactive vm testing during development, where tests might be broken - package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override { - buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; }); - }; - in - { - inherit package; - hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; }; - }; - }; - } - - ./test/interactive/configuration.nix - ./test/interactive/vm.nix - ./test/interactive/hakurei.nix - ./test/interactive/trace.nix - ./test/interactive/raceattr.nix - - self.nixosModules.hakurei - home-manager.nixosModules.home-manager - ]; - }; - in - pkgs.mkShell { - buildInputs = [ nixos.config.system.build.vm ]; - shellHook = "exec run-nixos-vm $@"; - }; - - generateDoc = - let - inherit (pkgs) lib; - - doc = - let - eval = lib.evalModules { - specialArgs = { - inherit pkgs; - }; - modules = [ (import ./options.nix self.packages) ]; - }; - cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; - in - pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; - docText = pkgs.runCommand "hakurei-module-docs.md" { } '' - cat ${doc.optionsCommonMark} > $out - sed -i '/*Declared by:*/,+1 d' $out - ''; - in - pkgs.mkShell { - shellHook = '' - exec cat ${docText} > options.md - ''; - }; - - generateSyscallTable = - let - GOARCH = { - x86_64-linux = "amd64"; - aarch64-linux = "arm64"; - }; - in - pkgs.mkShell { - shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" '' - set -e - ${pkgs.perl}/bin/perl \ - container/std/mksysnum_linux.pl \ - ${pkgs.linuxHeaders}/include/asm/unistd_64.h | \ - ${pkgs.go}/bin/gofmt > \ - container/std/syscall_linux_${GOARCH.${system}}.go - ''}"; - }; - } - ); - }; -} diff --git a/nixos.nix b/nixos.nix deleted file mode 100644 index 49bfffb6..00000000 --- a/nixos.nix +++ /dev/null @@ -1,407 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) - lists - attrsets - mkMerge - mkIf - mapAttrs - foldlAttrs - optional - optionals - ; - - cfg = config.environment.hakurei; - - # userid*userOffset + appStart + appid - getsubuid = userid: appid: userid * 100000 + 10000 + appid; - getsubname = userid: appid: "u${toString userid}_a${toString appid}"; - getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}"; - - mountpoints = { - ${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) { - depends = [ cfg.sharefs.source ]; - device = "sharefs"; - fsType = "fuse.sharefs"; - noCheck = true; - options = [ - "rw" - "noexec" - "nosuid" - "nodev" - "noatime" - "allow_other" - "mkdir" - "source=${cfg.sharefs.source}" - "setuid=${toString config.users.users.${cfg.sharefs.user}.uid}" - "setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}" - ]; - }; - }; -in - -{ - imports = [ (import ./options.nix packages) ]; - - options = { - # Forward declare a dummy option for VM filesystems since the real one won't exist - # unless the VM module is actually imported. - virtualisation.fileSystems = lib.mkOption { }; - }; - - config = mkIf cfg.enable { - assertions = [ - ( - let - conflictingApps = foldlAttrs ( - acc: id: app: - ( - acc - ++ foldlAttrs ( - acc': id': app': - if id == id' || app.shareUid && app'.shareUid || app.identity != app'.identity then acc' else acc' ++ [ id ] - ) [ ] cfg.apps - ) - ) [ ] cfg.apps; - in - { - assertion = (lists.length conflictingApps) == 0; - message = "the following hakurei apps have conflicting identities: " + (builtins.concatStringsSep ", " conflictingApps); - } - ) - ]; - - security.wrappers.hsu = { - source = "${cfg.hsuPackage}/bin/hsu"; - setuid = true; - owner = "root"; - group = "root"; - }; - - environment.etc.hsurc = { - mode = "0400"; - text = foldlAttrs ( - acc: username: fid: - "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc - ) "" cfg.users; - }; - - environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package; - fileSystems = mountpoints; - virtualisation.fileSystems = mountpoints; - - home-manager = - let - privPackages = mapAttrs (_: userid: { - home.packages = foldlAttrs ( - acc: id: app: - [ - ( - let - extendDBusDefault = id: ext: { - filter = true; - - talk = [ "org.freedesktop.Notifications" ] ++ ext.talk; - own = [ - "${id}.*" - "org.mpris.MediaPlayer2.${id}.*" - ] - ++ ext.own; - - inherit (ext) call broadcast; - }; - dbusConfig = - let - default = { - talk = [ ]; - own = [ ]; - call = { }; - broadcast = { }; - }; - in - { - session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault id)) else (extendDBusDefault id default); - system_bus = app.dbus.system; - }; - command = if app.command == null then app.name else app.command; - script = if app.script == null then ("exec " + command + " $@") else app.script; - isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; - - conf = { - inherit id; - inherit (app) identity enablements; - inherit (dbusConfig) session_bus system_bus; - direct_wayland = app.insecureWayland; - sched_policy = app.schedPolicy; - sched_priority = app.schedPriority; - groups = app.groups ++ optional (cfg.sharefs.source != null) cfg.sharefs.group; - - container = { - inherit (app) - wait_delay - devel - userns - device - tty - multiarch - env - ; - map_real_uid = app.mapRealUid; - host_net = app.hostNet; - host_abstract = app.hostAbstract; - share_runtime = app.shareRuntime; - share_tmpdir = app.shareTmpdir; - - filesystem = - let - bind = src: { - type = "bind"; - inherit src; - }; - optBind = src: { - type = "bind"; - inherit src; - optional = true; - }; - optDevBind = src: { - type = "bind"; - inherit src; - dev = true; - optional = true; - }; - in - [ - (bind "/bin") - (bind "/usr/bin") - (bind "/nix/store") - (optBind "/sys/block") - (optBind "/sys/bus") - (optBind "/sys/class") - (optBind "/sys/dev") - (optBind "/sys/devices") - ] - ++ optionals app.nix [ - (bind "/nix/var") - ] - ++ optionals isGraphical [ - (optDevBind "/dev/dri") - (optDevBind "/dev/nvidiactl") - (optDevBind "/dev/nvidia-modeset") - (optDevBind "/dev/nvidia-uvm") - (optDevBind "/dev/nvidia-uvm-tools") - (optDevBind "/dev/nvidia0") - ] - ++ optionals app.useCommonPaths cfg.commonPaths - ++ app.extraPaths - ++ [ - { - type = "bind"; - dst = "/etc/"; - src = "/etc/"; - special = true; - } - { - type = "link"; - dst = "/run/current-system"; - linkname = "/run/current-system"; - dereference = true; - } - ] - ++ optionals (isGraphical && config.hardware.graphics.enable) ( - [ - { - type = "link"; - dst = "/run/opengl-driver"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; - } - ] - ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ - { - type = "link"; - dst = "/run/opengl-driver-32"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; - } - ] - ) - ++ [ - { - type = "bind"; - src = getsubhome userid app.identity; - write = true; - ensure = true; - } - ]; - - username = getsubname userid app.identity; - inherit (cfg) shell; - home = getsubhome userid app.identity; - - path = - if app.path == null then - pkgs.writeScript "${app.name}-start" '' - #!${pkgs.zsh}${pkgs.zsh.shellPath} - ${script} - '' - else - app.path; - args = if app.args == null then [ "${app.name}-start" ] else app.args; - }; - }; - - checkedConfig = - name: value: - let - file = pkgs.writeText name (builtins.toJSON value); - in - pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } '' - ln -vs ${file} "$out" - hakurei show --no-store ${file} - ''; - in - pkgs.writeShellScriptBin app.name '' - exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ - '' - ) - ] - ++ ( - let - pkg = if app.share != null then app.share else pkgs.${app.name}; - copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; - in - optional (app.enablements.wayland || app.enablements.x11) ( - pkgs.runCommand "${app.name}-share" { } '' - mkdir -p $out/share - ${copy "${pkg}/share/applications"} - ${copy "${pkg}/share/pixmaps"} - ${copy "${pkg}/share/icons"} - ${copy "${pkg}/share/man"} - - if test -d "$out/share/applications"; then - substituteInPlace $out/share/applications/* \ - --replace-warn '${pkg}/bin/' "" \ - --replace-warn '${pkg}/libexec/' "" - fi - '' - ) - ) - ++ acc - ) [ cfg.package ] cfg.apps; - }) cfg.users; - in - { - useUserPackages = false; # prevent users.users entries from being added - - users = - mkMerge - (foldlAttrs - ( - acc: _: fid: - foldlAttrs - ( - acc: _: app: - ( - let - key = getsubname fid app.identity; - in - { - usernames = acc.usernames // { - ${key} = true; - }; - merge = acc.merge ++ [ - { - ${key} = mkMerge ( - [ - app.extraConfig - { home.packages = app.packages; } - ] - ++ lib.optional (!attrsets.hasAttrByPath [ key ] acc.usernames) cfg.extraHomeConfig - ); - } - ]; - } - ) - ) - { - inherit (acc) usernames; - merge = acc.merge ++ [ { ${getsubname fid 0} = cfg.extraHomeConfig; } ]; - } - cfg.apps - ) - { - usernames = { }; - merge = [ privPackages ]; - } - cfg.users - ).merge; - }; - - users = - let - getuser = userid: appid: { - isSystemUser = true; - createHome = true; - description = "Hakurei subordinate user ${toString appid} (u${toString userid})"; - group = getsubname userid appid; - home = getsubhome userid appid; - uid = getsubuid userid appid; - }; - getgroup = userid: appid: { gid = getsubuid userid appid; }; - in - { - users = mkMerge ( - foldlAttrs - ( - acc: username: fid: - acc - ++ - foldlAttrs - ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ] - ) - [ - { - ${getsubname fid 0} = getuser fid 0; - ${username}.extraGroups = [ cfg.sharefs.group ]; - } - ] - cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.user} = { - uid = lib.mkDefault 1023; - inherit (cfg.sharefs) group; - isSystemUser = true; - home = cfg.sharefs.source; - }; - }) - cfg.users - ); - - groups = mkMerge ( - foldlAttrs - ( - acc: _: fid: - acc - ++ foldlAttrs ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ] - ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.group} = { - gid = lib.mkDefault 1023; - }; - }) - cfg.users - ); - }; - }; -} diff --git a/options.md b/options.md deleted file mode 100644 index 6f6acddd..00000000 --- a/options.md +++ /dev/null @@ -1,1175 +0,0 @@ -## environment\.hakurei\.enable - - - -Whether to enable hakurei\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.package - - - -The hakurei package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix - -``` - - - -## environment\.hakurei\.apps - -Declaratively configured hakurei apps\. - - - -*Type:* -attribute set of (submodule) - - - -*Default:* - -```nix -{ } -``` - - - -## environment\.hakurei\.apps\.\\.enablements\.dbus - - - -Whether to proxy D-Bus\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.enablements\.pipewire - - - -Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.enablements\.wayland - - - -Whether to share the Wayland server via security-context-v1\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.enablements\.x11 - - - -Whether to share the X11 socket and allow connection\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -false -``` - - - -## environment\.hakurei\.apps\.\\.packages - - - -List of extra packages to install via home-manager\. - - - -*Type:* -list of package - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\\.args - - - -Custom args\. -Setting this to null will default to script name\. - - - -*Type:* -null or (list of string) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.command - - - -Command to run as the target user\. -Setting this to null will default command to launcher name\. -Has no effect when script is set\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.dbus\.session - - - -D-Bus session bus custom configuration\. -Setting this to null will enable built-in defaults\. - - - -*Type:* -null or (function that evaluates to a(n) anything) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.dbus\.system - - - -D-Bus system bus custom configuration\. -Setting this to null will disable the system bus proxy\. - - - -*Type:* -null or anything - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.devel - - - -Whether to enable debugging-related kernel interfaces\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.device - - - -Whether to enable access to all devices\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.env - - - -Environment variables to set for the initial process in the sandbox\. - - - -*Type:* -null or (attribute set of string) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.extraConfig - - - -Extra home-manager configuration\. - - - -*Type:* -anything - - - -*Default:* - -```nix -{ } -``` - - - -## environment\.hakurei\.apps\.\\.extraPaths - - - -Extra paths to make available to the container\. - - - -*Type:* -list of attribute set of anything - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\\.gpu - - - -Target process GPU and driver access\. -Setting this to null will enable GPU whenever X or Wayland is enabled\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.groups - - - -List of groups to inherit from the privileged user\. - - - -*Type:* -list of string - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\\.hostAbstract - - - -Whether to enable share abstract unix socket scope\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.hostNet - - - -Whether to enable share host net namespace\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -true -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.identity - - - -Application identity\. Identity 0 is reserved for system services\. - - - -*Type:* -integer between 1 and 9999 (both inclusive) - - - -## environment\.hakurei\.apps\.\\.insecureWayland - - - -Whether to enable direct access to the Wayland socket\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.mapRealUid - - - -Whether to enable mapping to priv-user uid\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.multiarch - - - -Whether to enable multiarch kernel-level support\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.name - - - -Name of the app’s launcher script\. - - - -*Type:* -string - - - -## environment\.hakurei\.apps\.\\.nix - - - -Whether to enable nix daemon access\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.path - - - -Custom executable path\. -Setting this to null will default to the start script\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.schedPolicy - - - -Scheduling policy to set for the container\. -The zero value retains the current scheduling policy\. - - - -*Type:* -null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext” - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.schedPriority - - - -Scheduling priority to set for the container\. - - - -*Type:* -null or integer between 1 and 99 (both inclusive) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.script - - - -Application launch script\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.share - - - -Package containing share files\. -Setting this to null will default package name to wrapper name\. - - - -*Type:* -null or package - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\\.shareRuntime - - - -Whether to enable sharing of XDG_RUNTIME_DIR between containers under the same identity\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.shareTmpdir - - - -Whether to enable sharing of TMPDIR between containers under the same identity\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.shareUid - - - -Whether to enable sharing identity with another application\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.tty - - - -Whether to enable access to the controlling terminal\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.useCommonPaths - - - -Whether to enable common extra paths\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -true -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.userns - - - -Whether to enable user namespace creation\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.verbose - - - -Whether to enable launchers with verbose output\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\\.wait_delay - - - -Duration to wait for after interrupting a container’s initial process in nanoseconds\. -A negative value causes the container to be terminated immediately on cancellation\. -Setting this to null defaults to five seconds\. - - - -*Type:* -null or signed integer - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.commonPaths - - - -Common extra paths to make available to the container\. - - - -*Type:* -list of attribute set of anything - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.extraHomeConfig - - - -Extra home-manager configuration to merge with all target users\. - - - -*Type:* -anything - - - -## environment\.hakurei\.hsuPackage - - - -The hsu package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix - -``` - - - -## environment\.hakurei\.sharefs\.package - - - -The sharefs package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix - -``` - - - -## environment\.hakurei\.sharefs\.group - - - -Name of the group to run the sharefs daemon as\. - - - -*Type:* -string - - - -*Default:* - -```nix -"sharefs" -``` - - - -## environment\.hakurei\.sharefs\.name - - - -Host path to mount sharefs on\. - - - -*Type:* -string - - - -*Default:* - -```nix -"/sdcard" -``` - - - -## environment\.hakurei\.sharefs\.source - - - -Writable backing directory\. Setting this to null disables sharefs\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.sharefs\.user - - - -Name of the user to run the sharefs daemon as\. - - - -*Type:* -string - - - -*Default:* - -```nix -"sharefs" -``` - - - -## environment\.hakurei\.shell - - - -Absolute path to preferred shell\. - - - -*Type:* -string - - - -*Default:* - -```nix -"/run/current-system/sw/bin/bash" -``` - - - -## environment\.hakurei\.stateDir - - - -The state directory where app home directories are stored\. - - - -*Type:* -string - - - -## environment\.hakurei\.users - - - -Users allowed to spawn hakurei apps and their corresponding hakurei identity\. - - - -*Type:* -attribute set of integer between 0 and 99 (both inclusive) - - - -*Default:* - -```nix -{ } -``` - - diff --git a/options.nix b/options.nix deleted file mode 100644 index f624b6f5..00000000 --- a/options.nix +++ /dev/null @@ -1,364 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) types mkOption mkEnableOption; - - cfg = config.environment.hakurei; -in - -{ - options = { - environment.hakurei = { - enable = mkEnableOption "hakurei"; - - package = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei; - description = "The hakurei package to use."; - }; - - hsuPackage = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hsu; - description = "The hsu package to use."; - }; - - users = mkOption { - type = - let - inherit (types) attrsOf ints; - in - attrsOf (ints.between 0 99); - description = '' - Users allowed to spawn hakurei apps and their corresponding hakurei identity. - ''; - }; - - extraHomeConfig = mkOption { - type = types.anything; - description = '' - Extra home-manager configuration to merge with all target users. - ''; - }; - - sharefs = { - package = mkOption { - type = types.package; - default = pkgs.linkFarm "sharefs" { - "bin/sharefs" = "${cfg.package}/libexec/sharefs"; - "bin/mount.fuse.sharefs" = "${cfg.package}/libexec/sharefs"; - }; - description = "The sharefs package to use."; - }; - - user = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the user to run the sharefs daemon as. - ''; - }; - - group = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the group to run the sharefs daemon as. - ''; - }; - - name = mkOption { - type = types.str; - default = "/sdcard"; - description = '' - Host path to mount sharefs on. - ''; - }; - - source = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Writable backing directory. Setting this to null disables sharefs. - ''; - }; - }; - - apps = mkOption { - type = - let - inherit (types) - int - ints - str - bool - enum - package - anything - submodule - listOf - attrsOf - nullOr - functionTo - ; - in - attrsOf (submodule { - options = { - name = mkOption { - type = str; - description = '' - Name of the app's launcher script. - ''; - }; - - verbose = mkEnableOption "launchers with verbose output"; - - identity = mkOption { - type = ints.between 1 9999; - description = '' - Application identity. Identity 0 is reserved for system services. - ''; - }; - shareUid = mkEnableOption "sharing identity with another application"; - - packages = mkOption { - type = listOf package; - default = [ ]; - description = '' - List of extra packages to install via home-manager. - ''; - }; - - extraConfig = mkOption { - type = anything; - default = { }; - description = '' - Extra home-manager configuration. - ''; - }; - - path = mkOption { - type = nullOr str; - default = null; - description = '' - Custom executable path. - Setting this to null will default to the start script. - ''; - }; - - args = mkOption { - type = nullOr (listOf str); - default = null; - description = '' - Custom args. - Setting this to null will default to script name. - ''; - }; - - script = mkOption { - type = nullOr str; - default = null; - description = '' - Application launch script. - ''; - }; - - command = mkOption { - type = nullOr str; - default = null; - description = '' - Command to run as the target user. - Setting this to null will default command to launcher name. - Has no effect when script is set. - ''; - }; - - groups = mkOption { - type = listOf str; - default = [ ]; - description = '' - List of groups to inherit from the privileged user. - ''; - }; - - shareRuntime = mkEnableOption "sharing of XDG_RUNTIME_DIR between containers under the same identity"; - shareTmpdir = mkEnableOption "sharing of TMPDIR between containers under the same identity"; - - dbus = { - session = mkOption { - type = nullOr (functionTo anything); - default = null; - description = '' - D-Bus session bus custom configuration. - Setting this to null will enable built-in defaults. - ''; - }; - - system = mkOption { - type = nullOr anything; - default = null; - description = '' - D-Bus system bus custom configuration. - Setting this to null will disable the system bus proxy. - ''; - }; - }; - - env = mkOption { - type = nullOr (attrsOf str); - default = null; - description = '' - Environment variables to set for the initial process in the sandbox. - ''; - }; - - wait_delay = mkOption { - type = nullOr int; - default = null; - description = '' - Duration to wait for after interrupting a container's initial process in nanoseconds. - A negative value causes the container to be terminated immediately on cancellation. - Setting this to null defaults to five seconds. - ''; - }; - - devel = mkEnableOption "debugging-related kernel interfaces"; - userns = mkEnableOption "user namespace creation"; - tty = mkEnableOption "access to the controlling terminal"; - multiarch = mkEnableOption "multiarch kernel-level support"; - - hostNet = mkEnableOption "share host net namespace" // { - default = true; - }; - hostAbstract = mkEnableOption "share abstract unix socket scope"; - - schedPolicy = mkOption { - type = nullOr (enum [ - "fifo" - "rr" - "batch" - "idle" - "deadline" - "ext" - ]); - default = null; - description = '' - Scheduling policy to set for the container. - The zero value retains the current scheduling policy. - ''; - }; - schedPriority = mkOption { - type = nullOr (ints.between 1 99); - default = null; - description = '' - Scheduling priority to set for the container. - ''; - }; - - nix = mkEnableOption "nix daemon access"; - mapRealUid = mkEnableOption "mapping to priv-user uid"; - device = mkEnableOption "access to all devices"; - insecureWayland = mkEnableOption "direct access to the Wayland socket"; - - gpu = mkOption { - type = nullOr bool; - default = null; - description = '' - Target process GPU and driver access. - Setting this to null will enable GPU whenever X or Wayland is enabled. - ''; - }; - - useCommonPaths = mkEnableOption "common extra paths" // { - default = true; - }; - - extraPaths = mkOption { - type = listOf (attrsOf anything); - default = [ ]; - description = '' - Extra paths to make available to the container. - ''; - }; - - enablements = { - wayland = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the Wayland server via security-context-v1. - ''; - }; - - x11 = mkOption { - type = nullOr bool; - default = false; - description = '' - Whether to share the X11 socket and allow connection. - ''; - }; - - dbus = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to proxy D-Bus. - ''; - }; - - pipewire = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket. - ''; - }; - }; - - share = mkOption { - type = nullOr package; - default = null; - description = '' - Package containing share files. - Setting this to null will default package name to wrapper name. - ''; - }; - }; - }); - default = { }; - description = '' - Declaratively configured hakurei apps. - ''; - }; - - commonPaths = mkOption { - type = types.listOf (types.attrsOf types.anything); - default = [ ]; - description = '' - Common extra paths to make available to the container. - ''; - }; - - shell = mkOption { - type = types.str; - default = "/run/current-system/sw/bin/bash"; - description = '' - Absolute path to preferred shell. - ''; - }; - - stateDir = mkOption { - type = types.str; - description = '' - The state directory where app home directories are stored. - ''; - }; - }; - }; -} diff --git a/package.nix b/package.nix deleted file mode 100644 index d426bf6c..00000000 --- a/package.nix +++ /dev/null @@ -1,144 +0,0 @@ -{ - lib, - stdenv, - buildGo126Module, - makeBinaryWrapper, - xdg-dbus-proxy, - pkg-config, - libffi, - libseccomp, - acl, - wayland, - wayland-protocols, - wayland-scanner, - - libxcb, - libxau, - libxdmcp, - - # for sharefs - fuse3, - - # for passthru.buildInputs - go_1_26, - clang, - xorgproto, - - # for check - util-linux, - nettools, - - glibc, # for ldd - withStatic ? stdenv.hostPlatform.isStatic, -}: - -buildGo126Module rec { - pname = "hakurei"; - version = with lib.strings; removePrefix "v" (trim (builtins.readFile ./cmd/dist/VERSION)); - - srcFiltered = builtins.path { - name = "${pname}-src"; - path = lib.cleanSource ./.; - filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/hsu" path); - }; - vendorHash = null; - - src = stdenv.mkDerivation { - name = "${pname}-src-full"; - inherit version; - enableParallelBuilding = true; - src = srcFiltered; - - buildInputs = [ - wayland - wayland-protocols - ]; - - nativeBuildInputs = [ - go_1_26 - pkg-config - wayland-scanner - ]; - - buildPhase = "GOCACHE=$(mktemp -d) go generate ./..."; - installPhase = "cp -r . $out"; - }; - - ldflags = - lib.attrsets.foldlAttrs - ( - ldflags: name: value: - ldflags ++ [ "-X hakurei.app/internal/info.${name}=${value}" ] - ) - ( - [ "-s -w" ] - ++ lib.optionals withStatic [ - "-linkmode external" - "-extldflags \"-static\"" - ] - ) - { - buildVersion = "v${version}"; - hakureiPath = "${placeholder "out"}/libexec/hakurei"; - hsuPath = "/run/wrappers/bin/hsu"; - }; - - env = { - # use clang instead of gcc - CC = "clang -O3 -Werror"; - }; - - buildInputs = [ - libffi - libseccomp - fuse3 - acl - wayland - - libxcb - libxau - libxdmcp - ]; - - nativeBuildInputs = [ - pkg-config - makeBinaryWrapper - - # for container example - nettools - ]; - - postInstall = - let - appPackages = [ - glibc - xdg-dbus-proxy - ]; - in - '' - install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/* - - mkdir "$out/libexec" - mv "$out"/bin/* "$out/libexec/" - - makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ - --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} - ''; - - passthru = { - go = go_1_26; - - targetPkgs = [ - go_1_26 - clang - xorgproto - util-linux - - # for go generate - wayland-protocols - wayland-scanner - ] - ++ buildInputs - ++ nativeBuildInputs; - }; -} diff --git a/test/default.nix b/test/default.nix index 76a490a7..81daa0a2 100644 --- a/test/default.nix +++ b/test/default.nix @@ -54,7 +54,7 @@ testers.nixosTest { # Run with Go race detector: environment.hakurei = lib.mkIf withRace rec { # race detector does not support static linking - package = (pkgs.callPackage ../package.nix { }).overrideAttrs (previousAttrs: { + package = (pkgs.callPackage ./package.nix { }).overrideAttrs (previousAttrs: { env = previousAttrs.env // { GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; }; diff --git a/test/flake.lock b/test/flake.lock new file mode 100644 index 00000000..ce9fe45f --- /dev/null +++ b/test/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780361225, + "narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "e28654b71096e08c019d4861ca26acb646f583d8", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-26.05", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1780453794, + "narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/test/flake.nix b/test/flake.nix new file mode 100644 index 00000000..6c8cf100 --- /dev/null +++ b/test/flake.nix @@ -0,0 +1,255 @@ +{ + description = "hakurei container tool and nixos module"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + + home-manager = { + url = "github:nix-community/home-manager/release-26.05"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + home-manager, + }: + let + supportedSystems = [ + "aarch64-linux" + "i686-linux" + "x86_64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + in + { + nixosModules.hakurei = import ./nixos.nix self.packages; + + checks = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + + inherit (pkgs) + runCommandLocal + callPackage + nixfmt + deadnix + statix + ; + in + { + hakurei = callPackage ./. { inherit system self; }; + race = callPackage ./. { + inherit system self; + withRace = true; + }; + + sandbox = callPackage ./sandbox { inherit self; }; + sandbox-race = callPackage ./sandbox { + inherit self; + withRace = true; + }; + + sharefs = callPackage ./sharefs { inherit system self; }; + + formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } '' + cd ${./.} + + echo "running nixfmt..." + nixfmt --width=256 --check . + + touch $out + ''; + + lint = + runCommandLocal "check-lint" + { + nativeBuildInputs = [ + deadnix + statix + ]; + } + '' + cd ${./.} + + echo "running deadnix..." + deadnix --fail + + echo "running statix..." + statix check . + + touch $out + ''; + } + ); + + packages = forAllSystems ( + system: + let + inherit (self.packages.${system}) hakurei hsu; + pkgs = nixpkgsFor.${system}; + in + { + default = hakurei; + hakurei = pkgs.pkgsStatic.callPackage ./package.nix { + inherit (pkgs) + # passthru.buildInputs + go_1_26 + clang + + # nativeBuildInputs + pkg-config + wayland-scanner + makeBinaryWrapper + + # appPackages + glibc + xdg-dbus-proxy + + # for check + util-linux + nettools + ; + }; + hsu = pkgs.callPackage ./hsu.nix { inherit (self.packages.${system}) hakurei; }; + sharefs = pkgs.linkFarm "sharefs" { + "bin/sharefs" = "${hakurei}/libexec/sharefs"; + "bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs"; + }; + + dist = + pkgs.runCommand "${hakurei.name}-dist" + { + buildInputs = hakurei.targetPkgs ++ [ + pkgs.pkgsStatic.musl + ]; + } + '' + cd $(mktemp -d) \ + && cp -r ${hakurei.src}/. . \ + && chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \ + && chmod -R +w . + + CC="musl-clang -O3 -Werror -Qunused-arguments" \ + GOCACHE="$(mktemp -d)" \ + PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ + DESTDIR="$out" \ + ./all.sh + ''; + } + ); + + devShells = forAllSystems ( + system: + let + inherit (self.packages.${system}) hakurei; + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + buildInputs = hakurei.targetPkgs; + hardeningDisable = [ "fortify" ]; + }; + withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; + + vm = + let + nixos = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + { + environment = { + systemPackages = [ + (pkgs.buildFHSEnv { + pname = "hakurei-fhs"; + inherit (hakurei) version; + targetPkgs = _: hakurei.targetPkgs; + extraOutputsToInstall = [ "dev" ]; + profile = '' + export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH" + ''; + }) + ]; + + hakurei = + let + # this is used for interactive vm testing during development, where tests might be broken + package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override { + buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; }); + }; + in + { + inherit package; + hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; }; + }; + }; + } + + ./interactive/configuration.nix + ./interactive/vm.nix + ./interactive/hakurei.nix + ./interactive/trace.nix + ./interactive/raceattr.nix + + self.nixosModules.hakurei + home-manager.nixosModules.home-manager + ]; + }; + in + pkgs.mkShell { + buildInputs = [ nixos.config.system.build.vm ]; + shellHook = "exec run-nixos-vm $@"; + }; + + generateDoc = + let + inherit (pkgs) lib; + + doc = + let + eval = lib.evalModules { + specialArgs = { + inherit pkgs; + }; + modules = [ (import ./options.nix self.packages) ]; + }; + cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; + in + pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; + docText = pkgs.runCommand "hakurei-module-docs.md" { } '' + cat ${doc.optionsCommonMark} > $out + sed -i '/*Declared by:*/,+1 d' $out + ''; + in + pkgs.mkShell { + shellHook = '' + exec cat ${docText} > options.md + ''; + }; + + generateSyscallTable = + let + GOARCH = { + x86_64-linux = "amd64"; + aarch64-linux = "arm64"; + }; + in + pkgs.mkShell { + shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" '' + set -e + ${pkgs.perl}/bin/perl \ + container/std/mksysnum_linux.pl \ + ${pkgs.linuxHeaders}/include/asm/unistd_64.h | \ + ${pkgs.go}/bin/gofmt > \ + container/std/syscall_linux_${GOARCH.${system}}.go + ''}"; + }; + } + ); + }; +} diff --git a/test/hsu.nix b/test/hsu.nix new file mode 100644 index 00000000..7acb3529 --- /dev/null +++ b/test/hsu.nix @@ -0,0 +1,23 @@ +{ + lib, + buildGoModule, + hakurei ? abort "hakurei package required", +}: + +buildGoModule { + pname = "${hakurei.pname}-hsu"; + inherit (hakurei) version; + + src = ../cmd/hsu; + inherit (hakurei) vendorHash; + env.CGO_ENABLED = 0; + + preBuild = '' + go mod init hsu >& /dev/null + ''; + + ldflags = lib.attrsets.foldlAttrs ( + ldflags: name: value: + ldflags ++ [ "-X main.${name}=${value}" ] + ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; }; +} diff --git a/test/interactive/raceattr.nix b/test/interactive/raceattr.nix index d94d7a7b..de54f4cf 100644 --- a/test/interactive/raceattr.nix +++ b/test/interactive/raceattr.nix @@ -16,7 +16,7 @@ src = builtins.path { name = "${pname}-src"; - path = lib.cleanSource ../../cmd/sharefs/test; + path = lib.cleanSource ../sharefs; filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path); }; vendorHash = null; diff --git a/test/nixos.nix b/test/nixos.nix new file mode 100644 index 00000000..49bfffb6 --- /dev/null +++ b/test/nixos.nix @@ -0,0 +1,407 @@ +packages: +{ + lib, + pkgs, + config, + ... +}: + +let + inherit (lib) + lists + attrsets + mkMerge + mkIf + mapAttrs + foldlAttrs + optional + optionals + ; + + cfg = config.environment.hakurei; + + # userid*userOffset + appStart + appid + getsubuid = userid: appid: userid * 100000 + 10000 + appid; + getsubname = userid: appid: "u${toString userid}_a${toString appid}"; + getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}"; + + mountpoints = { + ${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) { + depends = [ cfg.sharefs.source ]; + device = "sharefs"; + fsType = "fuse.sharefs"; + noCheck = true; + options = [ + "rw" + "noexec" + "nosuid" + "nodev" + "noatime" + "allow_other" + "mkdir" + "source=${cfg.sharefs.source}" + "setuid=${toString config.users.users.${cfg.sharefs.user}.uid}" + "setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}" + ]; + }; + }; +in + +{ + imports = [ (import ./options.nix packages) ]; + + options = { + # Forward declare a dummy option for VM filesystems since the real one won't exist + # unless the VM module is actually imported. + virtualisation.fileSystems = lib.mkOption { }; + }; + + config = mkIf cfg.enable { + assertions = [ + ( + let + conflictingApps = foldlAttrs ( + acc: id: app: + ( + acc + ++ foldlAttrs ( + acc': id': app': + if id == id' || app.shareUid && app'.shareUid || app.identity != app'.identity then acc' else acc' ++ [ id ] + ) [ ] cfg.apps + ) + ) [ ] cfg.apps; + in + { + assertion = (lists.length conflictingApps) == 0; + message = "the following hakurei apps have conflicting identities: " + (builtins.concatStringsSep ", " conflictingApps); + } + ) + ]; + + security.wrappers.hsu = { + source = "${cfg.hsuPackage}/bin/hsu"; + setuid = true; + owner = "root"; + group = "root"; + }; + + environment.etc.hsurc = { + mode = "0400"; + text = foldlAttrs ( + acc: username: fid: + "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc + ) "" cfg.users; + }; + + environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package; + fileSystems = mountpoints; + virtualisation.fileSystems = mountpoints; + + home-manager = + let + privPackages = mapAttrs (_: userid: { + home.packages = foldlAttrs ( + acc: id: app: + [ + ( + let + extendDBusDefault = id: ext: { + filter = true; + + talk = [ "org.freedesktop.Notifications" ] ++ ext.talk; + own = [ + "${id}.*" + "org.mpris.MediaPlayer2.${id}.*" + ] + ++ ext.own; + + inherit (ext) call broadcast; + }; + dbusConfig = + let + default = { + talk = [ ]; + own = [ ]; + call = { }; + broadcast = { }; + }; + in + { + session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault id)) else (extendDBusDefault id default); + system_bus = app.dbus.system; + }; + command = if app.command == null then app.name else app.command; + script = if app.script == null then ("exec " + command + " $@") else app.script; + isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; + + conf = { + inherit id; + inherit (app) identity enablements; + inherit (dbusConfig) session_bus system_bus; + direct_wayland = app.insecureWayland; + sched_policy = app.schedPolicy; + sched_priority = app.schedPriority; + groups = app.groups ++ optional (cfg.sharefs.source != null) cfg.sharefs.group; + + container = { + inherit (app) + wait_delay + devel + userns + device + tty + multiarch + env + ; + map_real_uid = app.mapRealUid; + host_net = app.hostNet; + host_abstract = app.hostAbstract; + share_runtime = app.shareRuntime; + share_tmpdir = app.shareTmpdir; + + filesystem = + let + bind = src: { + type = "bind"; + inherit src; + }; + optBind = src: { + type = "bind"; + inherit src; + optional = true; + }; + optDevBind = src: { + type = "bind"; + inherit src; + dev = true; + optional = true; + }; + in + [ + (bind "/bin") + (bind "/usr/bin") + (bind "/nix/store") + (optBind "/sys/block") + (optBind "/sys/bus") + (optBind "/sys/class") + (optBind "/sys/dev") + (optBind "/sys/devices") + ] + ++ optionals app.nix [ + (bind "/nix/var") + ] + ++ optionals isGraphical [ + (optDevBind "/dev/dri") + (optDevBind "/dev/nvidiactl") + (optDevBind "/dev/nvidia-modeset") + (optDevBind "/dev/nvidia-uvm") + (optDevBind "/dev/nvidia-uvm-tools") + (optDevBind "/dev/nvidia0") + ] + ++ optionals app.useCommonPaths cfg.commonPaths + ++ app.extraPaths + ++ [ + { + type = "bind"; + dst = "/etc/"; + src = "/etc/"; + special = true; + } + { + type = "link"; + dst = "/run/current-system"; + linkname = "/run/current-system"; + dereference = true; + } + ] + ++ optionals (isGraphical && config.hardware.graphics.enable) ( + [ + { + type = "link"; + dst = "/run/opengl-driver"; + linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; + } + ] + ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ + { + type = "link"; + dst = "/run/opengl-driver-32"; + linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; + } + ] + ) + ++ [ + { + type = "bind"; + src = getsubhome userid app.identity; + write = true; + ensure = true; + } + ]; + + username = getsubname userid app.identity; + inherit (cfg) shell; + home = getsubhome userid app.identity; + + path = + if app.path == null then + pkgs.writeScript "${app.name}-start" '' + #!${pkgs.zsh}${pkgs.zsh.shellPath} + ${script} + '' + else + app.path; + args = if app.args == null then [ "${app.name}-start" ] else app.args; + }; + }; + + checkedConfig = + name: value: + let + file = pkgs.writeText name (builtins.toJSON value); + in + pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } '' + ln -vs ${file} "$out" + hakurei show --no-store ${file} + ''; + in + pkgs.writeShellScriptBin app.name '' + exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ + '' + ) + ] + ++ ( + let + pkg = if app.share != null then app.share else pkgs.${app.name}; + copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; + in + optional (app.enablements.wayland || app.enablements.x11) ( + pkgs.runCommand "${app.name}-share" { } '' + mkdir -p $out/share + ${copy "${pkg}/share/applications"} + ${copy "${pkg}/share/pixmaps"} + ${copy "${pkg}/share/icons"} + ${copy "${pkg}/share/man"} + + if test -d "$out/share/applications"; then + substituteInPlace $out/share/applications/* \ + --replace-warn '${pkg}/bin/' "" \ + --replace-warn '${pkg}/libexec/' "" + fi + '' + ) + ) + ++ acc + ) [ cfg.package ] cfg.apps; + }) cfg.users; + in + { + useUserPackages = false; # prevent users.users entries from being added + + users = + mkMerge + (foldlAttrs + ( + acc: _: fid: + foldlAttrs + ( + acc: _: app: + ( + let + key = getsubname fid app.identity; + in + { + usernames = acc.usernames // { + ${key} = true; + }; + merge = acc.merge ++ [ + { + ${key} = mkMerge ( + [ + app.extraConfig + { home.packages = app.packages; } + ] + ++ lib.optional (!attrsets.hasAttrByPath [ key ] acc.usernames) cfg.extraHomeConfig + ); + } + ]; + } + ) + ) + { + inherit (acc) usernames; + merge = acc.merge ++ [ { ${getsubname fid 0} = cfg.extraHomeConfig; } ]; + } + cfg.apps + ) + { + usernames = { }; + merge = [ privPackages ]; + } + cfg.users + ).merge; + }; + + users = + let + getuser = userid: appid: { + isSystemUser = true; + createHome = true; + description = "Hakurei subordinate user ${toString appid} (u${toString userid})"; + group = getsubname userid appid; + home = getsubhome userid appid; + uid = getsubuid userid appid; + }; + getgroup = userid: appid: { gid = getsubuid userid appid; }; + in + { + users = mkMerge ( + foldlAttrs + ( + acc: username: fid: + acc + ++ + foldlAttrs + ( + acc': _: app: + acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ] + ) + [ + { + ${getsubname fid 0} = getuser fid 0; + ${username}.extraGroups = [ cfg.sharefs.group ]; + } + ] + cfg.apps + ) + (optional (cfg.sharefs.source != null) { + ${cfg.sharefs.user} = { + uid = lib.mkDefault 1023; + inherit (cfg.sharefs) group; + isSystemUser = true; + home = cfg.sharefs.source; + }; + }) + cfg.users + ); + + groups = mkMerge ( + foldlAttrs + ( + acc: _: fid: + acc + ++ foldlAttrs ( + acc': _: app: + acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ] + ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps + ) + (optional (cfg.sharefs.source != null) { + ${cfg.sharefs.group} = { + gid = lib.mkDefault 1023; + }; + }) + cfg.users + ); + }; + }; +} diff --git a/test/options.md b/test/options.md new file mode 100644 index 00000000..6f6acddd --- /dev/null +++ b/test/options.md @@ -0,0 +1,1175 @@ +## environment\.hakurei\.enable + + + +Whether to enable hakurei\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.package + + + +The hakurei package to use\. + + + +*Type:* +package + + + +*Default:* + +```nix + +``` + + + +## environment\.hakurei\.apps + +Declaratively configured hakurei apps\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* + +```nix +{ } +``` + + + +## environment\.hakurei\.apps\.\\.enablements\.dbus + + + +Whether to proxy D-Bus\. + + + +*Type:* +null or boolean + + + +*Default:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.enablements\.pipewire + + + +Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\. + + + +*Type:* +null or boolean + + + +*Default:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.enablements\.wayland + + + +Whether to share the Wayland server via security-context-v1\. + + + +*Type:* +null or boolean + + + +*Default:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.enablements\.x11 + + + +Whether to share the X11 socket and allow connection\. + + + +*Type:* +null or boolean + + + +*Default:* + +```nix +false +``` + + + +## environment\.hakurei\.apps\.\\.packages + + + +List of extra packages to install via home-manager\. + + + +*Type:* +list of package + + + +*Default:* + +```nix +[ ] +``` + + + +## environment\.hakurei\.apps\.\\.args + + + +Custom args\. +Setting this to null will default to script name\. + + + +*Type:* +null or (list of string) + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.command + + + +Command to run as the target user\. +Setting this to null will default command to launcher name\. +Has no effect when script is set\. + + + +*Type:* +null or string + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.dbus\.session + + + +D-Bus session bus custom configuration\. +Setting this to null will enable built-in defaults\. + + + +*Type:* +null or (function that evaluates to a(n) anything) + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.dbus\.system + + + +D-Bus system bus custom configuration\. +Setting this to null will disable the system bus proxy\. + + + +*Type:* +null or anything + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.devel + + + +Whether to enable debugging-related kernel interfaces\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.device + + + +Whether to enable access to all devices\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.env + + + +Environment variables to set for the initial process in the sandbox\. + + + +*Type:* +null or (attribute set of string) + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.extraConfig + + + +Extra home-manager configuration\. + + + +*Type:* +anything + + + +*Default:* + +```nix +{ } +``` + + + +## environment\.hakurei\.apps\.\\.extraPaths + + + +Extra paths to make available to the container\. + + + +*Type:* +list of attribute set of anything + + + +*Default:* + +```nix +[ ] +``` + + + +## environment\.hakurei\.apps\.\\.gpu + + + +Target process GPU and driver access\. +Setting this to null will enable GPU whenever X or Wayland is enabled\. + + + +*Type:* +null or boolean + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.groups + + + +List of groups to inherit from the privileged user\. + + + +*Type:* +list of string + + + +*Default:* + +```nix +[ ] +``` + + + +## environment\.hakurei\.apps\.\\.hostAbstract + + + +Whether to enable share abstract unix socket scope\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.hostNet + + + +Whether to enable share host net namespace\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +true +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.identity + + + +Application identity\. Identity 0 is reserved for system services\. + + + +*Type:* +integer between 1 and 9999 (both inclusive) + + + +## environment\.hakurei\.apps\.\\.insecureWayland + + + +Whether to enable direct access to the Wayland socket\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.mapRealUid + + + +Whether to enable mapping to priv-user uid\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.multiarch + + + +Whether to enable multiarch kernel-level support\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.name + + + +Name of the app’s launcher script\. + + + +*Type:* +string + + + +## environment\.hakurei\.apps\.\\.nix + + + +Whether to enable nix daemon access\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.path + + + +Custom executable path\. +Setting this to null will default to the start script\. + + + +*Type:* +null or string + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.schedPolicy + + + +Scheduling policy to set for the container\. +The zero value retains the current scheduling policy\. + + + +*Type:* +null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext” + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.schedPriority + + + +Scheduling priority to set for the container\. + + + +*Type:* +null or integer between 1 and 99 (both inclusive) + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.script + + + +Application launch script\. + + + +*Type:* +null or string + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.share + + + +Package containing share files\. +Setting this to null will default package name to wrapper name\. + + + +*Type:* +null or package + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.apps\.\\.shareRuntime + + + +Whether to enable sharing of XDG_RUNTIME_DIR between containers under the same identity\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.shareTmpdir + + + +Whether to enable sharing of TMPDIR between containers under the same identity\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.shareUid + + + +Whether to enable sharing identity with another application\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.tty + + + +Whether to enable access to the controlling terminal\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.useCommonPaths + + + +Whether to enable common extra paths\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +true +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.userns + + + +Whether to enable user namespace creation\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.verbose + + + +Whether to enable launchers with verbose output\. + + + +*Type:* +boolean + + + +*Default:* + +```nix +false +``` + + + +*Example:* + +```nix +true +``` + + + +## environment\.hakurei\.apps\.\\.wait_delay + + + +Duration to wait for after interrupting a container’s initial process in nanoseconds\. +A negative value causes the container to be terminated immediately on cancellation\. +Setting this to null defaults to five seconds\. + + + +*Type:* +null or signed integer + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.commonPaths + + + +Common extra paths to make available to the container\. + + + +*Type:* +list of attribute set of anything + + + +*Default:* + +```nix +[ ] +``` + + + +## environment\.hakurei\.extraHomeConfig + + + +Extra home-manager configuration to merge with all target users\. + + + +*Type:* +anything + + + +## environment\.hakurei\.hsuPackage + + + +The hsu package to use\. + + + +*Type:* +package + + + +*Default:* + +```nix + +``` + + + +## environment\.hakurei\.sharefs\.package + + + +The sharefs package to use\. + + + +*Type:* +package + + + +*Default:* + +```nix + +``` + + + +## environment\.hakurei\.sharefs\.group + + + +Name of the group to run the sharefs daemon as\. + + + +*Type:* +string + + + +*Default:* + +```nix +"sharefs" +``` + + + +## environment\.hakurei\.sharefs\.name + + + +Host path to mount sharefs on\. + + + +*Type:* +string + + + +*Default:* + +```nix +"/sdcard" +``` + + + +## environment\.hakurei\.sharefs\.source + + + +Writable backing directory\. Setting this to null disables sharefs\. + + + +*Type:* +null or string + + + +*Default:* + +```nix +null +``` + + + +## environment\.hakurei\.sharefs\.user + + + +Name of the user to run the sharefs daemon as\. + + + +*Type:* +string + + + +*Default:* + +```nix +"sharefs" +``` + + + +## environment\.hakurei\.shell + + + +Absolute path to preferred shell\. + + + +*Type:* +string + + + +*Default:* + +```nix +"/run/current-system/sw/bin/bash" +``` + + + +## environment\.hakurei\.stateDir + + + +The state directory where app home directories are stored\. + + + +*Type:* +string + + + +## environment\.hakurei\.users + + + +Users allowed to spawn hakurei apps and their corresponding hakurei identity\. + + + +*Type:* +attribute set of integer between 0 and 99 (both inclusive) + + + +*Default:* + +```nix +{ } +``` + + diff --git a/test/options.nix b/test/options.nix new file mode 100644 index 00000000..f624b6f5 --- /dev/null +++ b/test/options.nix @@ -0,0 +1,364 @@ +packages: +{ + lib, + pkgs, + config, + ... +}: + +let + inherit (lib) types mkOption mkEnableOption; + + cfg = config.environment.hakurei; +in + +{ + options = { + environment.hakurei = { + enable = mkEnableOption "hakurei"; + + package = mkOption { + type = types.package; + default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei; + description = "The hakurei package to use."; + }; + + hsuPackage = mkOption { + type = types.package; + default = packages.${pkgs.stdenv.hostPlatform.system}.hsu; + description = "The hsu package to use."; + }; + + users = mkOption { + type = + let + inherit (types) attrsOf ints; + in + attrsOf (ints.between 0 99); + description = '' + Users allowed to spawn hakurei apps and their corresponding hakurei identity. + ''; + }; + + extraHomeConfig = mkOption { + type = types.anything; + description = '' + Extra home-manager configuration to merge with all target users. + ''; + }; + + sharefs = { + package = mkOption { + type = types.package; + default = pkgs.linkFarm "sharefs" { + "bin/sharefs" = "${cfg.package}/libexec/sharefs"; + "bin/mount.fuse.sharefs" = "${cfg.package}/libexec/sharefs"; + }; + description = "The sharefs package to use."; + }; + + user = mkOption { + type = types.str; + default = "sharefs"; + description = '' + Name of the user to run the sharefs daemon as. + ''; + }; + + group = mkOption { + type = types.str; + default = "sharefs"; + description = '' + Name of the group to run the sharefs daemon as. + ''; + }; + + name = mkOption { + type = types.str; + default = "/sdcard"; + description = '' + Host path to mount sharefs on. + ''; + }; + + source = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Writable backing directory. Setting this to null disables sharefs. + ''; + }; + }; + + apps = mkOption { + type = + let + inherit (types) + int + ints + str + bool + enum + package + anything + submodule + listOf + attrsOf + nullOr + functionTo + ; + in + attrsOf (submodule { + options = { + name = mkOption { + type = str; + description = '' + Name of the app's launcher script. + ''; + }; + + verbose = mkEnableOption "launchers with verbose output"; + + identity = mkOption { + type = ints.between 1 9999; + description = '' + Application identity. Identity 0 is reserved for system services. + ''; + }; + shareUid = mkEnableOption "sharing identity with another application"; + + packages = mkOption { + type = listOf package; + default = [ ]; + description = '' + List of extra packages to install via home-manager. + ''; + }; + + extraConfig = mkOption { + type = anything; + default = { }; + description = '' + Extra home-manager configuration. + ''; + }; + + path = mkOption { + type = nullOr str; + default = null; + description = '' + Custom executable path. + Setting this to null will default to the start script. + ''; + }; + + args = mkOption { + type = nullOr (listOf str); + default = null; + description = '' + Custom args. + Setting this to null will default to script name. + ''; + }; + + script = mkOption { + type = nullOr str; + default = null; + description = '' + Application launch script. + ''; + }; + + command = mkOption { + type = nullOr str; + default = null; + description = '' + Command to run as the target user. + Setting this to null will default command to launcher name. + Has no effect when script is set. + ''; + }; + + groups = mkOption { + type = listOf str; + default = [ ]; + description = '' + List of groups to inherit from the privileged user. + ''; + }; + + shareRuntime = mkEnableOption "sharing of XDG_RUNTIME_DIR between containers under the same identity"; + shareTmpdir = mkEnableOption "sharing of TMPDIR between containers under the same identity"; + + dbus = { + session = mkOption { + type = nullOr (functionTo anything); + default = null; + description = '' + D-Bus session bus custom configuration. + Setting this to null will enable built-in defaults. + ''; + }; + + system = mkOption { + type = nullOr anything; + default = null; + description = '' + D-Bus system bus custom configuration. + Setting this to null will disable the system bus proxy. + ''; + }; + }; + + env = mkOption { + type = nullOr (attrsOf str); + default = null; + description = '' + Environment variables to set for the initial process in the sandbox. + ''; + }; + + wait_delay = mkOption { + type = nullOr int; + default = null; + description = '' + Duration to wait for after interrupting a container's initial process in nanoseconds. + A negative value causes the container to be terminated immediately on cancellation. + Setting this to null defaults to five seconds. + ''; + }; + + devel = mkEnableOption "debugging-related kernel interfaces"; + userns = mkEnableOption "user namespace creation"; + tty = mkEnableOption "access to the controlling terminal"; + multiarch = mkEnableOption "multiarch kernel-level support"; + + hostNet = mkEnableOption "share host net namespace" // { + default = true; + }; + hostAbstract = mkEnableOption "share abstract unix socket scope"; + + schedPolicy = mkOption { + type = nullOr (enum [ + "fifo" + "rr" + "batch" + "idle" + "deadline" + "ext" + ]); + default = null; + description = '' + Scheduling policy to set for the container. + The zero value retains the current scheduling policy. + ''; + }; + schedPriority = mkOption { + type = nullOr (ints.between 1 99); + default = null; + description = '' + Scheduling priority to set for the container. + ''; + }; + + nix = mkEnableOption "nix daemon access"; + mapRealUid = mkEnableOption "mapping to priv-user uid"; + device = mkEnableOption "access to all devices"; + insecureWayland = mkEnableOption "direct access to the Wayland socket"; + + gpu = mkOption { + type = nullOr bool; + default = null; + description = '' + Target process GPU and driver access. + Setting this to null will enable GPU whenever X or Wayland is enabled. + ''; + }; + + useCommonPaths = mkEnableOption "common extra paths" // { + default = true; + }; + + extraPaths = mkOption { + type = listOf (attrsOf anything); + default = [ ]; + description = '' + Extra paths to make available to the container. + ''; + }; + + enablements = { + wayland = mkOption { + type = nullOr bool; + default = true; + description = '' + Whether to share the Wayland server via security-context-v1. + ''; + }; + + x11 = mkOption { + type = nullOr bool; + default = false; + description = '' + Whether to share the X11 socket and allow connection. + ''; + }; + + dbus = mkOption { + type = nullOr bool; + default = true; + description = '' + Whether to proxy D-Bus. + ''; + }; + + pipewire = mkOption { + type = nullOr bool; + default = true; + description = '' + Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket. + ''; + }; + }; + + share = mkOption { + type = nullOr package; + default = null; + description = '' + Package containing share files. + Setting this to null will default package name to wrapper name. + ''; + }; + }; + }); + default = { }; + description = '' + Declaratively configured hakurei apps. + ''; + }; + + commonPaths = mkOption { + type = types.listOf (types.attrsOf types.anything); + default = [ ]; + description = '' + Common extra paths to make available to the container. + ''; + }; + + shell = mkOption { + type = types.str; + default = "/run/current-system/sw/bin/bash"; + description = '' + Absolute path to preferred shell. + ''; + }; + + stateDir = mkOption { + type = types.str; + description = '' + The state directory where app home directories are stored. + ''; + }; + }; + }; +} diff --git a/test/package.nix b/test/package.nix new file mode 100644 index 00000000..46567b69 --- /dev/null +++ b/test/package.nix @@ -0,0 +1,144 @@ +{ + lib, + stdenv, + buildGo126Module, + makeBinaryWrapper, + xdg-dbus-proxy, + pkg-config, + libffi, + libseccomp, + acl, + wayland, + wayland-protocols, + wayland-scanner, + + libxcb, + libxau, + libxdmcp, + + # for sharefs + fuse3, + + # for passthru.buildInputs + go_1_26, + clang, + xorgproto, + + # for check + util-linux, + nettools, + + glibc, # for ldd + withStatic ? stdenv.hostPlatform.isStatic, +}: + +buildGo126Module rec { + pname = "hakurei"; + version = with lib.strings; removePrefix "v" (trim (builtins.readFile ../cmd/dist/VERSION)); + + srcFiltered = builtins.path { + name = "${pname}-src"; + path = lib.cleanSource ../.; + filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/hsu" path); + }; + vendorHash = null; + + src = stdenv.mkDerivation { + name = "${pname}-src-full"; + inherit version; + enableParallelBuilding = true; + src = srcFiltered; + + buildInputs = [ + wayland + wayland-protocols + ]; + + nativeBuildInputs = [ + go_1_26 + pkg-config + wayland-scanner + ]; + + buildPhase = "GOCACHE=$(mktemp -d) go generate ./..."; + installPhase = "cp -r . $out"; + }; + + ldflags = + lib.attrsets.foldlAttrs + ( + ldflags: name: value: + ldflags ++ [ "-X hakurei.app/internal/info.${name}=${value}" ] + ) + ( + [ "-s -w" ] + ++ lib.optionals withStatic [ + "-linkmode external" + "-extldflags \"-static\"" + ] + ) + { + buildVersion = "v${version}"; + hakureiPath = "${placeholder "out"}/libexec/hakurei"; + hsuPath = "/run/wrappers/bin/hsu"; + }; + + env = { + # use clang instead of gcc + CC = "clang -O3 -Werror"; + }; + + buildInputs = [ + libffi + libseccomp + fuse3 + acl + wayland + + libxcb + libxau + libxdmcp + ]; + + nativeBuildInputs = [ + pkg-config + makeBinaryWrapper + + # for container example + nettools + ]; + + postInstall = + let + appPackages = [ + glibc + xdg-dbus-proxy + ]; + in + '' + install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/* + + mkdir "$out/libexec" + mv "$out"/bin/* "$out/libexec/" + + makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ + --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} + ''; + + passthru = { + go = go_1_26; + + targetPkgs = [ + go_1_26 + clang + xorgproto + util-linux + + # for go generate + wayland-protocols + wayland-scanner + ] + ++ buildInputs + ++ nativeBuildInputs; + }; +} diff --git a/test/sandbox/default.nix b/test/sandbox/default.nix index a0b8d13a..47a59de9 100644 --- a/test/sandbox/default.nix +++ b/test/sandbox/default.nix @@ -14,7 +14,7 @@ testers.nixosTest { # Run with Go race detector: environment.hakurei = lib.mkIf withRace rec { # race detector does not support static linking - package = (pkgs.callPackage ../../package.nix { }).overrideAttrs (previousAttrs: { + package = (pkgs.callPackage ../package.nix { }).overrideAttrs (previousAttrs: { env = previousAttrs.env // { GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; }; 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") -- cgit v1.3.1