diff options
Diffstat (limited to 'test')
35 files changed, 0 insertions, 4869 deletions
diff --git a/test/hakurei/configuration.nix b/test/hakurei/configuration.nix deleted file mode 100644 index 62d8239d..00000000 --- a/test/hakurei/configuration.nix +++ /dev/null @@ -1,252 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: -{ - users.users = { - alice = { - isNormalUser = true; - description = "Alice Foobar"; - password = "foobar"; - uid = 1000; - }; - untrusted = { - isNormalUser = true; - description = "Untrusted user"; - password = "foobar"; - uid = 1001; - - # For deny unmapped uid test: - packages = [ config.environment.hakurei.package ]; - }; - }; - - home-manager.users.alice.home.stateVersion = "24.11"; - - # Automatically login on tty1 as a normal user: - services.getty.autologinUser = "alice"; - - security.pam.loginLimits = [ - { - domain = "@users"; - item = "rtprio"; - type = "-"; - value = 1; - } - ]; - - environment = { - systemPackages = with pkgs; [ - # For D-Bus tests: - mako - libnotify - ]; - - variables = { - SWAYSOCK = "/tmp/sway-ipc.sock"; - WLR_RENDERER = "pixman"; - }; - - # To help with OCR: - etc."xdg/foot/foot.ini".text = lib.generators.toINI { } { - main = { - font = "inconsolata:size=14"; - }; - colors = rec { - foreground = "000000"; - background = "ffffff"; - regular2 = foreground; - }; - }; - }; - - fonts.packages = [ pkgs.inconsolata ]; - - # Automatically configure and start Sway when logging in on tty1: - programs.bash.loginShellInit = '' - if [ "$(tty)" = "/dev/tty1" ]; then - set -e - - mkdir -p ~/.config/sway - (sed s/Mod4/Mod1/ /etc/sway/config && - echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' && - echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config - - sway --validate - systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok - fi - ''; - - programs.sway.enable = true; - - # For PulseAudio tests: - security.rtkit.enable = true; - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - }; - - virtualisation = { - # Hopefully reduces spurious test failures: - memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192; - - qemu.options = [ - # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch: - "-vga none -device virtio-gpu-pci" - - # Increase Go test compiler performance: - "-smp 16" - ]; - }; - - # Disk image is too small for some tests: - boot.tmp.useTmpfs = true; - - environment.hakurei = { - enable = true; - stateDir = "/var/lib/hakurei"; - users.alice = 0; - - extraHomeConfig = - { config, ... }: - { - # To test merge deduplication: - options._hakurei.stateVersion = lib.mkOption { type = lib.types.str; }; - - config = { - home = { inherit (config._hakurei) stateVersion; }; - _hakurei.stateVersion = "23.05"; - }; - }; - - commonPaths = [ - { - type = "bind"; - src = "/var/tmp"; - write = true; - } - ]; - - apps = { - "cat.gensokyo.extern.bash.linger-timeout" = { - name = "hakurei-check-linger-timeout"; - identity = 9999; - share = pkgs.bash; - packages = [ pkgs.bash ]; - command = '' - sleep infinity & disown - exit - ''; - wait_delay = 1; - enablements = { - wayland = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements" = { - name = "ne-foot"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements.immediate" = { - name = "ne-foot-immediate"; - identity = 1; - shareUid = true; - verbose = true; - wait_delay = -1; - share = pkgs.foot; - packages = [ ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.pulseaudio" = { - name = "pa-foot"; - identity = 2; - verbose = true; - share = pkgs.foot; - packages = [ pkgs.foot ]; - command = "foot"; - enablements.dbus = false; - }; - - "cat.gensokyo.extern.Alacritty.x11" = { - name = "x11-alacritty"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.alacritty; - packages = with pkgs; [ - # For X11 terminal emulator: - alacritty - - # For glinfo: - mesa-demos - ]; - command = "alacritty"; - enablements = { - wayland = false; - x11 = true; - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.directWayland" = { - name = "da-foot"; - identity = 4; - verbose = true; - insecureWayland = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.strace.wantFail" = { - name = "strace-failure"; - identity = 5; - verbose = true; - share = pkgs.strace; - command = "strace true"; - enablements = { - wayland = false; - x11 = false; - dbus = false; - pipewire = false; - }; - }; - }; - }; -} diff --git a/test/hakurei/default.nix b/test/hakurei/default.nix deleted file mode 100644 index 81daa0a2..00000000 --- a/test/hakurei/default.nix +++ /dev/null @@ -1,81 +0,0 @@ -{ - lib, - testers, - buildFHSEnv, - writeShellScriptBin, - - system, - self, - withRace ? false, -}: - -testers.nixosTest { - name = "hakurei" + (if withRace then "-race" else ""); - nodes.machine = - { options, pkgs, ... }: - let - fhs = - let - hakurei = options.environment.hakurei.package.default; - in - 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: - (writeShellScriptBin "hakurei-test" '' - # Assert hst CGO_ENABLED=0: ${ - with pkgs; - runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } '' - cp -r ${options.environment.hakurei.package.default.src} "$out" - chmod -R +w "$out" - cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go" - (cd "$out" && HOME="$(mktemp -d)" CGO_ENABLED=0 go test .) - '' - } - - cd ${self.packages.${system}.hakurei.src} - ${fhs}/bin/hakurei-fhs -c \ - 'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \ - &> /tmp/hakurei-test.log && \ - touch /tmp/hakurei-test-ok - touch /tmp/hakurei-test-done - '') - ]; - - # 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: { - env = previousAttrs.env // { - GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; - }; - }); - hsuPackage = options.environment.hakurei.hsuPackage.default.override { hakurei = package; }; - }; - - imports = [ - ./configuration.nix - - self.nixosModules.hakurei - self.inputs.home-manager.nixosModules.home-manager - ]; - }; - - # adapted from nixos sway integration tests - - # testScriptWithTypes:49: error: Cannot call function of unknown type - # (machine.succeed if succeed else machine.execute)( - # ^ - # Found 1 error in 1 file (checked 1 source file) - skipTypeCheck = true; - testScript = builtins.readFile ./test.py; -} diff --git a/test/hakurei/flake.lock b/test/hakurei/flake.lock deleted file mode 100644 index 5537506a..00000000 --- a/test/hakurei/flake.lock +++ /dev/null @@ -1,49 +0,0 @@ -{ - "nodes": { - "home-manager": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1787146702, - "narHash": "sha256-YbRcLdU/yK4gWsQg7V8WTKZHfXL33g8+wSFUX3wyevs=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "173b7e8d40fdc8c296a9c99854314f17a3a1704c", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "release-26.05", - "repo": "home-manager", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1787101114, - "narHash": "sha256-gwrPcFf/rDjHPaVflbDZ040ZDmBTRj/7+s8ZmE2SaIM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b18a4b905f8d028dc4476412e6d6891728695379", - "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/hakurei/flake.nix b/test/hakurei/flake.nix deleted file mode 100644 index dc42b1cb..00000000 --- a/test/hakurei/flake.nix +++ /dev/null @@ -1,76 +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 = [ "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) callPackage; - in - { - hakurei = callPackage ./. { inherit system self; }; - race = callPackage ./. { - inherit system self; - withRace = true; - }; - } - ); - - 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_27 - 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; }; - } - ); - }; -} diff --git a/test/hakurei/hsu.nix b/test/hakurei/hsu.nix deleted file mode 100644 index 5dd4cf5d..00000000 --- a/test/hakurei/hsu.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - 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/hakurei/nixos.nix b/test/hakurei/nixos.nix deleted file mode 100644 index 49bfffb6..00000000 --- a/test/hakurei/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/test/hakurei/options.nix b/test/hakurei/options.nix deleted file mode 100644 index f624b6f5..00000000 --- a/test/hakurei/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/test/hakurei/package.nix b/test/hakurei/package.nix deleted file mode 100644 index 0facf291..00000000 --- a/test/hakurei/package.nix +++ /dev/null @@ -1,144 +0,0 @@ -{ - lib, - stdenv, - buildGo127Module, - 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_27, - clang, - xorgproto, - - # for check - util-linux, - nettools, - - glibc, # for ldd - withStatic ? stdenv.hostPlatform.isStatic, -}: - -buildGo127Module 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_27 - 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_27; - - targetPkgs = [ - go_1_27 - clang - xorgproto - util-linux - - # for go generate - wayland-protocols - wayland-scanner - ] - ++ buildInputs - ++ nativeBuildInputs; - }; -} diff --git a/test/hakurei/test.py b/test/hakurei/test.py deleted file mode 100644 index 98f08275..00000000 --- a/test/hakurei/test.py +++ /dev/null @@ -1,315 +0,0 @@ -import json -import shlex - -q = shlex.quote -NODE_GROUPS = ["nodes", "floating_nodes"] - - -def swaymsg(command: str = "", succeed=True, type="command"): - assert command != "" or type != "command", "Must specify command or type" - shell = q(f"swaymsg -t {q(type)} -- {q(command)}") - with machine.nested(f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)): - ret = (machine.succeed if succeed else machine.execute)( - f"su - alice -c {shell}" - ) - - # execute also returns a status code, but disregard. - if not succeed: - _, ret = ret - - if not succeed and not ret: - return None - - parsed = json.loads(ret) - return parsed - - -def walk(tree): - yield tree - for group in NODE_GROUPS: - for node in tree.get(group, []): - yield from walk(node) - - -def wait_for_window(pattern): - def func(last_chance): - nodes = (node["name"] for node in walk(swaymsg(type="get_tree"))) - - if last_chance: - nodes = list(nodes) - machine.log(f"Last call! Current list of windows: {nodes}") - - return any(pattern in name for name in nodes) - - retry(func) - - -def collect_state_ui(name): - swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'") - machine.wait_for_file(f"/tmp/{name}.ps") - machine.copy_from_vm(f"/tmp/{name}.ps", "") - swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'") - machine.wait_for_file(f"/tmp/{name}.json") - machine.copy_from_vm(f"/tmp/{name}.json", "") - machine.screenshot(name) - - -def check_state(name, enablements): - instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps")) - if len(instances) != 1: - raise Exception(f"unexpected state length {len(instances)}") - instance = instances[0] - - command = f"{name}-start" - if not (instance['container']['path'].startswith("/nix/store/")) or not (instance['container']['path'].endswith(command)): - raise Exception(f"unexpected path {instance['path']}") - - if len(instance['container']['args']) != 1 or instance['container']['args'][0] != command: - raise Exception(f"unexpected args {instance['args']}") - - if instance['enablements'] != enablements: - raise Exception(f"unexpected enablements {instance['enablements']['enablements']}") - - -def hakurei(command): - swaymsg(f"exec hakurei {command}") - - -start_all() -machine.wait_for_unit("multi-user.target") - -# To check hakurei's version: -print(machine.succeed("sudo -u alice -i hakurei version")) - -# Wait for Sway to complete startup: -machine.wait_for_file("/run/user/1000/wayland-1") -machine.wait_for_file("/tmp/sway-ipc.sock") - -# Run hakurei Go tests outside of nix build in the background: -swaymsg("exec hakurei-test") - -# Deny unmapped uid: -denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout") -print(denyOutput) -denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout") -print(denyOutputVerbose) - -# Direct hsu call: -userid = machine.succeed("sudo -u alice -i hsu") -if userid != "0": - raise Exception(f"unexpected userid: {userid}") - -# Verify hsu fault behaviour: -if denyOutput != "hsu: uid 1001 is not in the hsurc file\n": - raise Exception(f"unexpected deny output:\n{denyOutput}") -if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n": - raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}") - -# Verify timeout behaviour: -machine.succeed('sudo -u alice -i hakurei-check-linger-timeout > /var/tmp/linger-stdout 2> /var/tmp/linger-stderr || (cat /var/tmp/linger-stderr; false)') -linger_stdout = machine.succeed("cat /var/tmp/linger-stdout") -linger_stderr = machine.succeed("cat /var/tmp/linger-stderr") -if linger_stdout != "": - raise Exception(f"unexpected stdout: {linger_stdout}") -if linger_stderr != "init: timeout exceeded waiting for lingering processes\n": - raise Exception(f"unexpected stderr: {linger_stderr}") - -check_offset = 0 - - -def hakurei_identity(offset): - return 1+check_offset+offset - - -# Start hakurei permissive defaults outside Wayland session: -print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok")) -machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok") - -# Verify silent output permissive defaults: -output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout") -if output != "": - raise Exception(f"unexpected output\n{output}") - -# Verify silent output permissive defaults signal: -def silent_output_interrupt(flags): - swaymsg("exec foot") - wait_for_window("alice@machine") - # identity 0 does not have home-manager - machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n") - machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.send_key("ctrl-c") - machine.wait_until_fails("pgrep foot") - machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '") - output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent") - if output != "": - raise Exception(f"unexpected output\n{output}") - - -silent_output_interrupt("") -silent_output_interrupt("--dbus ") # this one is especially painful as it maintains a helper -silent_output_interrupt("--wayland -X --dbus --pulse ") - -# Verify graceful failure on bad Wayland display name: -print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true")) - -# Start hakurei permissive defaults within Wayland session: -hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok') -machine.wait_for_file("/tmp/dbus-ok") -collect_state_ui("dbus_notify_exited") -# not in pid namespace, verify termination -machine.wait_until_fails("pgrep xdg-dbus-proxy") -machine.succeed("pkill -9 mako") - -# Check revert type selection: -hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok") -wait_for_window("p0@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok") -wait_for_window("p1@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p1-exit-ok") -# Verify acl is kept alive: -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p0-exit-ok") -machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000") - -# Check invalid identifier fd behaviour: -machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd') -machine.wait_for_file("/tmp/invalid-identifier-fd") -print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd')) - -# Check interrupt shim behaviour: -swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 230: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check interrupt shim behaviour immediate termination: -swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 254: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check shim SIGCONT from unexpected process behaviour: -swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -CONT -f 'hakurei shim'") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/shim-cont-unexpected-pid") -print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid')) - -# Check setscheduler: -sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_unset != 0: - raise Exception(f"unexpected unset policy: {sched_unset}") -sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_idle != 5: - raise Exception(f"unexpected idle policy: {sched_idle}") -sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_rr != 2: - raise Exception(f"unexpected round-robin policy: {sched_idle}") - -# Start app (foot) with Wayland enablement: -swaymsg("exec ne-foot") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/client-ok\n") -machine.wait_for_file("/var/tmp/client-ok") -collect_state_ui("foot_wayland") -check_state("ne-foot", {"wayland": True}) -# Verify lack of acl on XDG_RUNTIME_DIR: -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") - -# Test pipewire-pulse: -swaymsg("exec pa-foot") -wait_for_window(f"u0_a{hakurei_identity(1)}@machine") -machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n") -machine.wait_for_file("/var/tmp/pulse-ok") -collect_state_ui("pulse_wayland") -check_state("pa-foot", {"wayland": True, "pipewire": True}) -machine.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.wait_until_fails("pgrep -x hakurei") -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -# Test PipeWire SecurityContext: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle") -# Test PipeWire direct access: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump") - -# Test XWayland (foot does not support X): -swaymsg("exec x11-alacritty") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; glinfo && touch /var/tmp/x11-ok\n") -machine.wait_for_file("/var/tmp/x11-ok") -collect_state_ui("alacritty_x11") -check_state("x11-alacritty", {"x11": True}) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep alacritty") - -# Start app (foot) with direct Wayland access: -swaymsg("exec da-foot") -wait_for_window(f"u0_a{hakurei_identity(3)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/direct-ok\n") -collect_state_ui("foot_direct") -machine.wait_for_file("/var/tmp/direct-ok") -check_state("da-foot", {"wayland": True}) -# Verify acl on XDG_RUNTIME_DIR: -print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}")) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -# Verify acl cleanup on XDG_RUNTIME_DIR: -machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}") - -# Test syscall filter: -print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure")) - -# Start app (foot) with Wayland enablement from a terminal: -swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/term-ok\n") -machine.wait_for_file("/var/tmp/term-ok") -machine.send_key("alt-h") -machine.send_chars("clear; hakurei show $(hakurei ps --short) && touch /tmp/ps-show-ok && exec cat\n") -machine.wait_for_file("/tmp/ps-show-ok") -collect_state_ui("foot_wayland_term") -check_state("ne-foot", {"wayland": True}) -machine.send_key("alt-l") -machine.send_chars("exit\n") -wait_for_window("alice@machine") -machine.send_key("ctrl-c") -machine.wait_until_fails("pgrep foot") - -# Exit Sway and verify process exit status 0: -machine.wait_until_fails("pgrep -x hakurei") -swaymsg("exit", succeed=False) -machine.wait_for_file("/tmp/sway-exit-ok") - -# Print hakurei share and rundir contents: -print(machine.succeed("find /tmp/hakurei.0 " - + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o " - + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o " - + "-print")) -print(machine.succeed("find /run/user/1000/hakurei")) -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") - -# Verify go test status: -machine.wait_for_file("/tmp/hakurei-test-done") -print(machine.succeed("cat /tmp/hakurei-test.log")) -machine.wait_for_file("/tmp/hakurei-test-ok") diff --git a/test/internal/mountinfo/mountinfo.go b/test/internal/mountinfo/mountinfo.go deleted file mode 100644 index 19892cd2..00000000 --- a/test/internal/mountinfo/mountinfo.go +++ /dev/null @@ -1,177 +0,0 @@ -// Package mountinfo provides util-linux bindings for parsing -// proc_pid_mountinfo(5). -// -// This package must never be used outside integration tests, a much better -// implementation can be found in package vfs. -// -// Attempting to import this package outside testing causes the resulting -// program to panic. -package mountinfo - -/* -#cgo linux pkg-config: --static mount - -#include <stdlib.h> -#include <stdio.h> -#include <libmount.h> - -const char *HAKUREI_MOUNTINFO_PATH = "/proc/self/mountinfo"; -*/ -import "C" - -import ( - "errors" - "fmt" - "runtime" - "unsafe" -) - -var ( - // ErrParse is returned by [Open] when encountering a bad record. - ErrParse = errors.New("invalid mountinfo record") - // ErrIter is returned by [Open] if an iterator cannot be allocated. - ErrIter = errors.New("cannot allocate iterator") - // ErrIterAdvance is stored when the iterator is unable to advance. - ErrIterAdvance = errors.New("unable to advance iterator") -) - -type ( - // Iter refers to libmnt iterator state. - Iter struct { - // Last stored error. - err error - // Whether iteration has concluded. - ok bool - // Whether Close had already been called. - closed bool - - tb *C.struct_libmnt_table - itr *C.struct_libmnt_iter - - fs *C.struct_libmnt_fs - } - - // Entry represents deterministic mountinfo parts of a libmnt_fs entry. - Entry struct { - // mount ID: a unique ID for the mount (may be reused after umount(2)). - ID int `json:"id"` - // parent ID: the ID of the parent mount (or of self for the root of - // this mount namespace's mount tree). - Parent int `json:"parent"` - // root: the pathname of the directory in the filesystem which forms the - // root of this mount. - Root string `json:"root"` - // mount point: the pathname of the mount point relative to the - // process's root directory. - Target string `json:"target"` - // mount options: per-mount options (see mount(2)). - VfsOptstr string `json:"vfs_optstr"` - // filesystem type: the filesystem type in the form "type[.subtype]". - FsType string `json:"fstype"` - // mount source: filesystem-specific information or "none". - Source string `json:"source"` - // super options: per-superblock options (see mount(2)). - FsOptstr string `json:"fs_optstr"` - } -) - -// Copy populates v with the current record. -func (m *Iter) Copy(v *Entry) { - if m.fs == nil { - panic("invalid entry") - } - v.ID = int(C.mnt_fs_get_id(m.fs)) - v.Parent = int(C.mnt_fs_get_parent_id(m.fs)) - v.Root = C.GoString(C.mnt_fs_get_root(m.fs)) - v.Target = C.GoString(C.mnt_fs_get_target(m.fs)) - v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs)) - v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs)) - v.Source = C.GoString(C.mnt_fs_get_source(m.fs)) - v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs)) -} - -// Err returns the saved iterator error. -func (m *Iter) Err() error { return m.err } - -// Open opens a mountinfo document. If name is an empty string, the mountinfo -// document of the current process is opened instead. -func Open(name string) (*Iter, error) { - var m Iter - if name == "" { - m.tb = C.mnt_new_table_from_file(C.HAKUREI_MOUNTINFO_PATH) - } else { - _name := C.CString(name) - m.tb = C.mnt_new_table_from_file(_name) - C.free(unsafe.Pointer(_name)) - } - if m.tb == nil { - return nil, ErrParse - } - m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD) - if m.itr == nil { - C.mnt_unref_table(m.tb) - return nil, ErrIter - } - m.ok = true - - runtime.SetFinalizer(&m, (*Iter).Close) - return &m, nil -} - -// Close frees the iterator. -func (m *Iter) Close() { - if m.closed { - return - } - if m.tb == nil { - panic("unref called before open") - } - - C.mnt_unref_table(m.tb) - C.mnt_free_iter(m.itr) - m.closed = true - runtime.SetFinalizer(m, nil) -} - -// Reset resets the iterator to the first record for reuse. -func (m *Iter) Reset() { - if m.err != nil { - panic("attempting to reset a faulted iterator") - } - m.ok = true - C.mnt_reset_iter(m.itr, -1) -} - -// Next advances the iterator to the next record. The record may be copied if -// Next returns true. -func (m *Iter) Next() bool { - if !m.ok || m.err != nil { - return false - } - - r := C.mnt_table_next_fs(m.tb, m.itr, &m.fs) - if r < 0 { - m.err = ErrIterAdvance - } - m.ok = r == 0 - return m.ok -} - -// EqualWithIgnore compares e with want, ignoring fields with the specified -// ignore value. -func (e *Entry) EqualWithIgnore(want *Entry, ignore string) bool { - return (e.ID == want.ID || want.ID == -1) && - (e.Parent == want.Parent || want.Parent == -1) && - (e.Root == want.Root || want.Root == ignore) && - (e.Target == want.Target || want.Target == ignore) && - (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && - (e.FsType == want.FsType || want.FsType == ignore) && - (e.Source == want.Source || want.Source == ignore) && - (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) -} - -// String returns a text representation of e loosely following the kernel format. -func (e *Entry) String() string { - return fmt.Sprintf("%d %d %s %s %s %s %s %s", - e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr) -} diff --git a/test/internal/mountinfo/mountinfo_guard.go b/test/internal/mountinfo/mountinfo_guard.go deleted file mode 100644 index aaf63ac4..00000000 --- a/test/internal/mountinfo/mountinfo_guard.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !testsuite && !tester - -package mountinfo - -import ( - "os" - "testing" -) - -func init() { - if !testing.Testing() { - println("package mountinfo imported in non-testsuite program") - os.Exit(1) - } -} diff --git a/test/internal/mountinfo/mountinfo_test.go b/test/internal/mountinfo/mountinfo_test.go deleted file mode 100644 index 45cfdf5f..00000000 --- a/test/internal/mountinfo/mountinfo_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package mountinfo_test - -import ( - "os" - "path/filepath" - "testing" - - "hakurei.app/test/internal/mountinfo" -) - -func TestMountinfo(t *testing.T) { - testCases := []struct { - name string - - sample string - want []*mountinfo.Entry - }{ - {"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw -16 20 0:15 / /sys rw,relatime - sysfs /sys rw -17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 -18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw -20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd -23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset -24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns -25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu -26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct -27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory -28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices -29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer -30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls -31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio -32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw -38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw -39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw -40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered -41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered -42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw -43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw -44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500 -45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw -47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344 -49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*mountinfo.Entry{ - e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"), - e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"), - e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"), - e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"), - e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"), - e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"), - e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"), - e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"), - e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"), - e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"), - e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"), - e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"), - e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"), - e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"), - e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"), - e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"), - e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"), - e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), - e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), - e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), - e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), - e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), - e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"), - e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"), - e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"), - e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"), - e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"), - e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"), - e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"), - e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"), - e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"), - e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"), - e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"), - }}, - } - - for _, tc := range testCases { - name := filepath.Join(t.TempDir(), "sample") - if err := os.WriteFile(name, []byte(tc.sample), 0400); err != nil { - t.Fatalf("cannot write sample: %v", err) - } - - t.Run(tc.name, func(t *testing.T) { - m, err := mountinfo.Open(name) - if err != nil { - t.Fatalf("Open: error = %v", err) - } - t.Cleanup(m.Close) - - i := 0 - var ent mountinfo.Entry - for m.Next() { - m.Copy(&ent) - - if i == len(tc.want) { - t.Errorf("Next: got more than %d entries", i) - t.FailNow() - } - if !ent.EqualWithIgnore(tc.want[i], "\x00") { - t.Errorf("Next: entry %d\n got: %#v\nwant: %#v", i, - ent, &tc.want[i]) - t.FailNow() - } else { - t.Logf("%s", &ent) - } - - i++ - } - - if err = m.Err(); err != nil { - t.Fatalf("Err: %v", err) - } - }) - - if err := os.Remove(name); err != nil { - t.Fatalf("cannot remove %q: %v", name, err) - } - } -} - -func e( - id, parent int, - root, target, vfsOptstr string, - fsType, source, fsOptstr string, -) *mountinfo.Entry { - return &mountinfo.Entry{ - ID: id, - Parent: parent, - Root: root, - Target: target, - VfsOptstr: vfsOptstr, - FsType: fsType, - Source: source, - FsOptstr: fsOptstr, - } -} diff --git a/test/internal/testsuite/fs.go b/test/internal/testsuite/fs.go deleted file mode 100644 index 9acd24b6..00000000 --- a/test/internal/testsuite/fs.go +++ /dev/null @@ -1,129 +0,0 @@ -package testsuite - -import ( - "errors" - "fmt" - "io/fs" - "path/filepath" - "strings" -) - -var ( - // ErrFSBadLength is returned by [FS.Compare] for a directory with an - // unexpected amount of dents. - ErrFSBadLength = errors.New("bad dir length") - // ErrFSBadData is returned by [FS.Compare] for a file with unexpected - // contents. - ErrFSBadData = errors.New("data differs") - // ErrFSBadMode is returned by [FS.Compare] for an entry with unexpected - // mode. - ErrFSBadMode = errors.New("mode differs") - // ErrFSInvalidEnt is returned by [FS.Compare] if an invalid [FS] is visited. - ErrFSInvalidEnt = errors.New("invalid entry condition") -) - -// FS represents part of a filesystem hierarchy. -type FS struct { - // Expected mode of corresponding entry. - Mode fs.FileMode `json:"mode"` - // Expected directory contents. The directory is not descended if Dir is nil. - Dir map[string]*FS `json:"dir"` - // Expected file contents. The file is not read if Data is nil. - Data *string `json:"data"` -} - -// dprintf calls printf if it is non-nil. -func dprintf(printf func(format string, a ...any), format string, a ...any) { - if printf == nil { - return - } - printf(format, a...) -} - -// printDir prints a failed [FS.Compare] directory. -func printDir( - printf func(format string, a ...any), - prefix string, - dir []fs.DirEntry, -) { - names := make([]string, len(dir)) - for i, ent := range dir { - name := ent.Name() - if ent.IsDir() { - name += "/" - } - names[i] = fmt.Sprintf("%q", name) - } - dprintf(printf, "[FAIL] d %s: %s", prefix, strings.Join(names, " ")) -} - -// Compare compares the contents of prefix against the hierarchy described by s. -func (s *FS) Compare( - printf func(format string, a ...any), - prefix string, - e fs.FS, -) error { - if s.Data != nil { - if s.Dir != nil { - panic("invalid state") - } - panic("invalid compare call") - } - - if s.Dir == nil { - dprintf(printf, "[ OK ] s %s", prefix) - return nil - } - - var dir []fs.DirEntry - if d, err := fs.ReadDir(e, prefix); err != nil { - return err - } else if len(d) != len(s.Dir) { - printDir(printf, prefix, d) - return ErrFSBadLength - } else { - dir = d - } - - for _, got := range dir { - name := got.Name() - - if want, ok := s.Dir[name]; !ok { - printDir(printf, prefix, dir) - return fs.ErrNotExist - } else if want.Dir != nil && !got.IsDir() { - printDir(printf, prefix, dir) - return ErrFSInvalidEnt - } else { - name = filepath.Join(prefix, name) - - if fi, err := got.Info(); err != nil { - return err - } else if fi.Mode() != want.Mode { - dprintf(printf, "[FAIL] m %s: %#o, want %#o", - name, uint32(fi.Mode()), uint32(want.Mode)) - return ErrFSBadMode - } - - if want.Data != nil { - if want.Dir != nil { - panic("invalid state") - } - if v, err := fs.ReadFile(e, name); err != nil { - return err - } else if string(v) != *want.Data { - dprintf(printf, - "[FAIL] f %s\n\t got: %s\n\twant: %s", - name, v, *want.Data, - ) - return ErrFSBadData - } - dprintf(printf, "[ OK ] f %s", name) - } else if err := want.Compare(printf, name, e); err != nil { - return err - } - } - } - dprintf(printf, "[ OK ] d %s", prefix) - return nil -} diff --git a/test/internal/testsuite/fs_test.go b/test/internal/testsuite/fs_test.go deleted file mode 100644 index 5c93bb46..00000000 --- a/test/internal/testsuite/fs_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package testsuite_test - -import ( - "bytes" - "errors" - "fmt" - "io/fs" - "testing" - "testing/fstest" - - "hakurei.app/test/internal/testsuite" -) - -func TestCompare(t *testing.T) { - var ( - fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh" - fsGroupSample = "hakurei:x:65534:" - ) - - testCases := []struct { - name string - - sample fstest.MapFS - want *testsuite.FS - wantOut string - wantErr error - }{ - {"skip", fstest.MapFS{}, &testsuite.FS{}, "[ OK ] s .\x00", nil}, - {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0x800001ed}}}, - "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil}, - {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &testsuite.FS{Dir: make(map[string]*testsuite.FS)}, - "[FAIL] d .: \".hakurei/\"\x00", testsuite.ErrFSBadLength}, - {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0xdeadbeef}}}, - "[FAIL] m .hakurei: 020000000755, want 033653337357\x00", testsuite.ErrFSBadMode}, - {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, - &testsuite.FS{Dir: map[string]*testsuite.FS{"test": {Dir: make(map[string]*testsuite.FS)}}}, - "[FAIL] d .: \"test\"\x00", testsuite.ErrFSInvalidEnt}, - {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, - &testsuite.FS{Dir: map[string]*testsuite.FS{".test": {}}}, - "[FAIL] d .: \"test\"\x00", fs.ErrNotExist}, - {"file", fstest.MapFS{"etc": {Mode: 0x800001c0}, - "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, - "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, - }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{ - "passwd": {Mode: 0x1a4, Data: &fsPasswdSample}, - "group": {Mode: 0x1a4, Data: &fsGroupSample}, - }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil}, - {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0}, - "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, - "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, - }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{ - "passwd": {Mode: 0x1a4, Data: &fsGroupSample}, - "group": {Mode: 0x1a4, Data: &fsGroupSample}, - }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\n\t got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\n\twant: hakurei:x:65534:\x00", testsuite.ErrFSBadData}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var buf bytes.Buffer - - err := tc.want.Compare( - func(format string, a ...any) { - _, _ = fmt.Fprintf(&buf, format+"\x00", a...) - }, - ".", tc.sample, - ) - if !errors.Is(err, tc.wantErr) { - t.Errorf( - "Compare: error = %v; wantErr %v", - err, tc.wantErr, - ) - } - - if buf.String() != tc.wantOut { - t.Errorf( - "Compare: output %q; want %q", - &buf, tc.wantOut, - ) - } - }) - } -} diff --git a/test/internal/testsuite/proc.go b/test/internal/testsuite/proc.go deleted file mode 100644 index e7ef1aed..00000000 --- a/test/internal/testsuite/proc.go +++ /dev/null @@ -1,353 +0,0 @@ -package testsuite - -import ( - "bytes" - "errors" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - "syscall" - "unsafe" - - "hakurei.app/fhs" -) - -// Stat represents status information read from /proc/pid/stat. -type Stat struct { - // The process ID. - PID int - // The filename of the executable, with parenthesis stripped. - Comm string - // One of the following characters, indicating process state: - // - // R Running - // - // S Sleeping in an interruptible wait - // - // D Waiting in uninterruptible disk sleep - // - // Z Zombie - // - // T Stopped (on a signal) or (before Linux - // 2.6.33) trace stopped - // - // t Tracing stop (Linux 2.6.33 onward) - // - // W Paging (only before Linux 2.6.0) - // - // X Dead (from Linux 2.6.0 onward) - // - // x Dead (Linux 2.6.33 to 3.13 only) - // - // K Wakekill (Linux 2.6.33 to 3.13 only) - // - // W Waking (Linux 2.6.33 to 3.13 only) - // - // P Parked (Linux 3.9 to 3.13 only) - // - // I Idle (Linux 4.14 onward) - State byte - // The process ID of the parent of this process. - PPID int - // The process group ID of the process. - PGRP int - // The session ID of the process. - Session int - // The controlling terminal of the process. - TTYNR int - // The ID of the foreground process group of the controlling terminal of the - // process. - TPGID int - // The kernel flags word of the process. For bit meanings, see the PF_* - // defines in the Linux kernel source file include/linux/sched.h. - Flags uint - // The number of minor faults the process has made which have not required - // loading a memory page from disk. - MinFlt uint - // The number of minor faults that the process's waited-for children have - // made. - CMinFlt uint - // The number of major faults the process has made which have required - // loading a memory page from disk. - MajFlt uint - // The number of major faults that the process's waited-for children have - // made. - CMajFlt uint - // Amount of time that this process has been scheduled in user mode, - // measured in clock ticks. - UTime uint - // Amount of time that this process has been scheduled in kernel mode, - // measured in clock ticks. - STime uint - // Amount of time that this process's waited-for children have been - // scheduled in user mode, measured in clock ticks. - CUTime int - // Amount of time that this process's waited-for children have been - // scheduled in kernel mode, measured in clock ticks. - CSTime int - // For processes running a real-time scheduling policy, this is the negated - // scheduling priority, minus one. - Priority int - // The nice value, a value in the range 19 (low priority) to -20 (high - // priority). - Nice int - // Number of threads in this process. - NumThreads int - - // unmaintained field: itrealvalue - - // The time the process started after system boot. Since Linux 2.6, the - // value is expressed in clock ticks. - StartTime uint64 - // Virtual memory size in bytes. - VSize uint - // Resident set size in pages. - RSS int - // Soft limit in bytes on the rss of the process. - RSSLim uint64 - // The address above which program text can run. - StartCode uint64 - // The address below which program text can run. - EndCode uint64 - // The address of the start (i.e., bottom) of the stack. - StartStack uint64 - // The current value of ESP (stack pointer), as found in the kernel stack - // page for the process. - KSTKESP uint64 - // The current EIP (instruction pointer). - KSTKEIP uint64 - - // obsolete fields: signal, blocked, sigignore, sigcatch - - // This is the "channel" in which the process is waiting. It is the address - // of a location in the kernel where the process is sleeping. - WChan uint64 - - // unmaintained fields: nswap, cnswap - - // Signal to be sent to parent when we die. - ExitSignal int - // CPU number last executed on. - Processor int - // Real-time scheduling priority, a number in the range 1 to 99 for processes - // scheduled under a real-time policy, or 0, for non-real-time processes. - RTPriority uint - // Scheduling policy (see sched_setscheduler(2)). Decode using the SCHED_* - // constants in linux/sched.h. - Policy uint - // Aggregated block I/O delays, measured in clock ticks (centiseconds). - DelayAcctBlkIOTicks uint64 - // Guest time of the process (time spent running a virtual CPU for a guest - // operating system), measured in clock ticks. - GuestTime int - // Guest time of the process's children, measured in clock ticks. - CGuestTime int -} - -// Executable is like [os.Executable], but for the process referred to by s. -func (s *Stat) Executable() (string, error) { - path, err := os.Readlink(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "exe")) - - // When the executable has been deleted then Readlink returns a - // path appended with " (deleted)". - return strings.TrimSuffix(path, " (deleted)"), err -} - -// Stat populates stat with the proc filesystem entry referred to by s. -func (s *Stat) Stat(stat *syscall.Stat_t) (err error) { - err = syscall.Stat(filepath.Join(fhs.Proc, strconv.Itoa(s.PID)), stat) - if err != nil { - err = os.NewSyscallError("stat", err) - } - return -} - -// Args reads arguments of the process referred to by s. -func (s *Stat) Args() ([]string, error) { - p, err := os.ReadFile(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "cmdline")) - if err != nil { - return nil, err - } - a := bytes.Split(p, []byte{0}) - if len(a) > 0 && len(a[len(a)-1]) == 0 { - a = a[:len(a)-1] - } - - args := make([]string, len(a)) - for i, arg := range a { - args[i] = unsafe.String(unsafe.SliceData(arg), len(arg)) - } - return args, nil -} - -// ErrBadDelimiters is returned by [Stat.UnmarshalText] if one or both bytes of -// the comm delimiter pair were missing or misplaced. -var ErrBadDelimiters = errors.New("missing comm delimiters") - -// UnmarshalText populates the structure pointed to by s from text. -func (s *Stat) UnmarshalText(text []byte) (err error) { - var ( - discard uint64 - _uint64 = &discard - _int64 = (*int64)(unsafe.Pointer(&discard)) - - ld = bytes.Index(text, []byte("(")) - rd = bytes.LastIndex(text, []byte(")")) - ) - - if ld <= 0 || rd < 0 { - return ErrBadDelimiters - } - - if s.PID, err = strconv.Atoi( - unsafe.String(unsafe.SliceData(text), ld-1), - ); err != nil { - return - } - - s.Comm = string(text[ld+1 : rd]) - - var ( - n int - - state string - ) - n, err = fmt.Fscan( - bytes.NewBuffer(text[rd+2:]), - &state, - &s.PPID, - &s.PGRP, - &s.Session, - &s.TTYNR, - &s.TPGID, - &s.Flags, - &s.MinFlt, - &s.CMinFlt, - &s.MajFlt, - &s.CMajFlt, - &s.UTime, - &s.STime, - &s.CUTime, - &s.CSTime, - &s.Priority, - &s.Nice, - &s.NumThreads, - _int64, - &s.StartTime, - &s.VSize, - &s.RSS, - &s.RSSLim, - &s.StartCode, - &s.EndCode, - &s.StartStack, - &s.KSTKESP, - &s.KSTKEIP, - _uint64, - _uint64, - _uint64, - _uint64, - &s.WChan, - _uint64, - _uint64, - &s.ExitSignal, - &s.Processor, - &s.RTPriority, - &s.Policy, - &s.DelayAcctBlkIOTicks, - &s.GuestTime, - &s.CGuestTime, - ) - if err != nil { - err = fmt.Errorf("field %d: %w", n, err) - } else if len(state) != 1 { - err = fmt.Errorf("invalid state %q", state) - } else { - s.State = state[0] - } - return -} - -// A StatScanner continuously scans the proc filesystem for process status -// information in /proc/pid/stat. -type StatScanner struct { - // Current entry. - stat Stat - // Cached top-level /proc entries. - dents []os.DirEntry - // Current progress through dents. - i int - // Whether the previous call to Scan had repopulated dents. - wrapped bool - // First stored error: a non-nil err disables the scanner. - err error -} - -// IsNotExist returns whether an error is [os.ErrNotExist] or ESRCH. -func IsNotExist(err error) bool { - return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH) -} - -// Scan reads a process status information entry. It returns false if an -// unrecoverable error is encountered, after which Scan no longer scans new -// entries. -func (s *StatScanner) Scan() bool { - if s.err != nil { - return false - } - - if s.wrapped = s.i == len(s.dents); s.wrapped { - if s.dents, s.err = os.ReadDir(fhs.Proc); s.err != nil { - return false - } - s.i = 0 - if len(s.dents) == 0 { - s.err = syscall.ENOTRECOVERABLE - return false - } - } - - for s.i < len(s.dents) { - dent := s.dents[s.i] - s.i++ - if !dent.IsDir() { - continue - } - - pid, err := strconv.Atoi(dent.Name()) - if err != nil { - continue - } - - var p []byte - p, err = os.ReadFile(filepath.Join(fhs.Proc, dent.Name(), "stat")) - if err != nil { - if IsNotExist(err) { - continue - } - s.err = err - return false - } - - s.err = s.stat.UnmarshalText(p) - if s.err == nil && pid != s.stat.PID { - s.err = fmt.Errorf( - "bad status information: dent=%d, stat=%d", - pid, s.stat.PID, - ) - } - return s.err == nil - } - return s.Scan() -} - -// Stat returns the address of the [Stat] structure populated by the last call -// to Scan. -func (s *StatScanner) Stat() *Stat { return &s.stat } - -// Err returns the stored error value. -func (s *StatScanner) Err() error { return s.err } - -// Repopulated returns whether the last Scan call had re-read the proc filesystem. -func (s *StatScanner) Repopulated() bool { return s.wrapped } diff --git a/test/internal/testsuite/proc_test.go b/test/internal/testsuite/proc_test.go deleted file mode 100644 index a8698e73..00000000 --- a/test/internal/testsuite/proc_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package testsuite_test - -import ( - "testing" - - "hakurei.app/test/internal/testsuite" -) - -func BenchmarkStatScanner(b *testing.B) { - var s testsuite.StatScanner - - for b.Loop() { - if !s.Scan() { - b.Fatal(s.Err()) - } - } -} - -func BenchmarkStatScannerFull(b *testing.B) { - var s testsuite.StatScanner - - for b.Loop() { - for s.Scan() { - if s.Repopulated() { - break - } - } - - if err := s.Err(); err != nil { - b.Fatal(err) - } - } -} diff --git a/test/internal/testsuite/ptrace.go b/test/internal/testsuite/ptrace.go deleted file mode 100644 index ccf0900c..00000000 --- a/test/internal/testsuite/ptrace.go +++ /dev/null @@ -1,144 +0,0 @@ -package testsuite - -import ( - "crypto/sha512" - "encoding/base64" - "errors" - "fmt" - "os" - "syscall" - "unsafe" -) - -const ( - // _PTRACE_ATTACH attaches to the process specified in pid. - _PTRACE_ATTACH = 16 - // _PTRACE_DETACH restarts the stopped tracee as for PTRACE_CONT, but first - // detaches from it. - _PTRACE_DETACH = 17 - - // _PTRACE_SECCOMP_GET_FILTER allows the tracer to dump the tracee's classic - // BPF filters. - _PTRACE_SECCOMP_GET_FILTER = 0x420c -) - -// ptrace wraps the ptrace syscall. -func ptrace( - op uintptr, - pid, addr int, - data unsafe.Pointer, -) (r uintptr, errno syscall.Errno) { - r, _, errno = syscall.Syscall6( - syscall.SYS_PTRACE, - op, - uintptr(pid), - uintptr(addr), - uintptr(data), - 0, 0, - ) - return -} - -// ptraceAttach attaches to the process referred to by pid. -func ptraceAttach(pid int) error { - if _, errno := ptrace(_PTRACE_ATTACH, pid, 0, nil); errno != 0 { - return os.NewSyscallError("PTRACE_ATTACH", errno) - } - - var status syscall.WaitStatus - for { - if _, err := syscall.Wait4( - pid, - &status, - syscall.WALL, - nil, - ); err != nil { - if errors.Is(err, syscall.EINTR) { - continue - } - return os.NewSyscallError("wait4", err) - } - switch { - case status.Stopped(): - return nil - - case status.Continued(): - continue - - case status.Signaled(): - return fmt.Errorf( - "tracee terminated by signal %s", - status.Signal(), - ) - - case status.Exited(): - return fmt.Errorf( - "tracee terminated unexpectedly with code %d", - status.ExitStatus(), - ) - } - } -} - -// ptraceDetach detaches from the attached process referred to by pid. -func ptraceDetach(pid int) error { - if _, errno := ptrace(_PTRACE_DETACH, pid, 0, nil); errno != 0 { - return os.NewSyscallError("PTRACE_DETACH", errno) - } - return nil -} - -// getFilter dumps the specified tracee's cBPF filter at the specified index -// and returns the resulting payload. T must be eight bytes long and must not -// contain pointers. -func getFilter(pid, index int) ([]syscall.SockFilter, error) { - var buf []syscall.SockFilter - if n, errno := ptrace( - _PTRACE_SECCOMP_GET_FILTER, - pid, index, nil, - ); errno != 0 { - return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) - } else { - buf = make([]syscall.SockFilter, n) - } - if _, errno := ptrace( - _PTRACE_SECCOMP_GET_FILTER, - pid, index, unsafe.Pointer(&buf[0]), - ); errno != 0 { - return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) - } - return buf, nil -} - -// CheckFilter checks the process at pid to have its first filter's contents -// match the specified sha512 checksum. -func CheckFilter(pid, index int, sum [sha512.Size]byte) (err error) { - if err = ptraceAttach(pid); err != nil { - return - } - defer func() { - if detachErr := ptraceDetach(pid); err == nil { - err = detachErr - } - }() - - var buf []syscall.SockFilter - h := sha512.New() - if buf, err = getFilter(pid, index); err != nil { - return - } else { - h.Write(unsafe.Slice( - (*byte)(unsafe.Pointer(&buf[0])), - uintptr(len(buf))*unsafe.Sizeof(buf[0]), - )) - } - - if got := h.Sum(nil); string(got) != string(sum[:]) { - return fmt.Errorf( - "bad filter\n\t got: %s\n\twant: %s", - base64.StdEncoding.EncodeToString(got), - base64.StdEncoding.EncodeToString(sum[:]), - ) - } - return -} diff --git a/test/internal/testsuite/ptrace_test.go b/test/internal/testsuite/ptrace_test.go deleted file mode 100644 index eaaed131..00000000 --- a/test/internal/testsuite/ptrace_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package testsuite - -import ( - "syscall" - "testing" - "unsafe" -) - -func TestBlockSize(t *testing.T) { - if sz := unsafe.Sizeof(syscall.SockFilter{}); sz != 8 { - t.Fatalf("invalid filter block size %d", sz) - } -} diff --git a/test/internal/testsuite/testsuite.go b/test/internal/testsuite/testsuite.go deleted file mode 100644 index 456c36b4..00000000 --- a/test/internal/testsuite/testsuite.go +++ /dev/null @@ -1,290 +0,0 @@ -// Package testsuite provides many quick-and-dirty integration testing utilities. -// -// Attempting to import this package outside testing causes the resulting -// program to panic. -package testsuite - -import ( - "bufio" - "context" - "crypto/sha512" - "errors" - "log" - "os" - "os/exec" - "os/signal" - "sync" - "syscall" - "time" -) - -// ReceiveSignals blocks until a termination signal arrives, and terminates. -func ReceiveSignals() { - s := make(chan os.Signal, 3) - signal.Notify(s, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) - log.Fatalf("terminating on signal %s", <-s) -} - -// MustRun runs command and terminates the testsuite on error. -func MustRun(cred *syscall.Credential, extraEnv []string, command ...string) { - cmd := exec.Command(command[0], command[1:]...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGKILL, - Credential: cred, - } - if len(extraEnv) != 0 { - cmd.Env = append(cmd.Environ(), extraEnv...) - } - if err := cmd.Run(); err != nil { - log.Fatal(err) - } -} - -// ErrUnexpectedSuccess is returned for processes expected to exit with a -// non-zero code, but failed to do so. -var ErrUnexpectedSuccess = errors.New("process unexpectedly exited with code 0") - -// MustFail runs command and terminates the testsuite if the program fails to -// start or exits with code 0. -func MustFail(cred *syscall.Credential, extraEnv []string, command ...string) { - cmd := exec.Command(command[0], command[1:]...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGKILL, - Credential: cred, - } - if len(extraEnv) != 0 { - cmd.Env = append(cmd.Environ(), extraEnv...) - } - if err := cmd.Run(); err == nil { - log.Fatal(ErrUnexpectedSuccess) - } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { - log.Fatal(err) - } else if !e.Exited() { - log.Fatal(e) - } -} - -// MustStart starts cmd and returns a channel delivering its wait error. -func MustStart(cmd *exec.Cmd) (done <-chan error) { - if err := cmd.Start(); err != nil { - log.Fatal(err) - } - d := make(chan error) - go func() { d <- cmd.Wait() }() - return d -} - -// MustStartWith wraps [MustStart] and creates the [exec.Cmd] object internally. -func MustStartWith( - ctx context.Context, - cred *syscall.Credential, - extraEnv []string, - files []*os.File, - command ...string, -) (proc *os.Process, done <-chan error) { - cmd := exec.CommandContext(ctx, command[0], command[1:]...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - cmd.ExtraFiles = files - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - Credential: cred, - } - if len(extraEnv) != 0 { - cmd.Env = append(cmd.Environ(), extraEnv...) - } - return cmd.Process, MustStart(cmd) -} - -// MustCheckFilter is like [CheckFilter], but terminates the test suite if a -// non-nil error is returned. Otherwise, the tracee is terminated after it -// resumes. -func MustCheckFilter(pid int, sum [sha512.Size]byte) { - // podman installs its own filter - if err := CheckFilter(pid, 1, sum); err != nil { - log.Fatal(err) - } else if err = syscall.Kill(pid, syscall.SIGTERM); err != nil { - log.Fatalf("cannot terminate tracee: %v", err) - } -} - -// FilterTerminated returns a non-nil error if err is not an [exec.ExitError] -// describing a process terminated by a syscall.SIGTERM signal. -func FilterTerminated(err error) error { - if err == nil { - return ErrUnexpectedSuccess - } - - e, ok := errors.AsType[*exec.ExitError](err) - if !ok { - return err - } - - if e.ExitCode() == 0x80+int(syscall.SIGTERM) { - return nil - } - return e -} - -// Poll repeatedly runs command until it succeeds. -func Poll( - d time.Duration, - cred *syscall.Credential, - extraEnv []string, - command ...string, -) { - for range time.NewTicker(d).C { - cmd := exec.Command(command[0], command[1:]...) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGKILL, - Credential: cred, - } - if len(extraEnv) != 0 { - cmd.Env = append(cmd.Environ(), extraEnv...) - } - if err := cmd.Run(); err != nil { - if e, ok := errors.AsType[*exec.ExitError](err); ok && e.Exited() { - continue - } - log.Fatal(err) - } - break - } -} - -const ( - // XDGRuntimeDir is the hardcoded XDG runtime directory for the user - // described by [GetUser]. - XDGRuntimeDir = "/var/run/user/1000" - - // XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR. - XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir -) - -// MustStartSessionBus starts a session bus that is never explicitly terminated. -// The test suite is terminated if the session bus daemon terminates. -func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) { - r, w, err := os.Pipe() - if err != nil { - log.Fatal(err) - } - - // this is never explicitly terminated - _, done := MustStartWith( - context.Background(), cred, nil, []*os.File{w}, - "dbus-daemon", - "--print-address=3", - "--address=unix:path="+XDGRuntimeDir+"/dbus", - "--session", - "--nofork", - "--nopidfile", - ) - - go func() { - if _err := <-done; _err != nil { - log.Fatal(_err) - } - log.Fatal("session bus terminated unexpectedly") - }() - - dbusEnv, err = bufio.NewReader(r).ReadString('\n') - if err != nil { - log.Fatal(err) - } - dbusEnv = dbusEnv[:len(dbusEnv)-1] - log.Printf("dbus listening on %s", dbusEnv) - dbusEnv = "DBUS_SESSION_BUS_ADDRESS=" + dbusEnv - - if err = r.Close(); err != nil { - log.Fatal(err) - } - return -} - -const ( - // SwayEnv is the environment variable string for the sway IPC socket. - SwayEnv = "SWAYSOCK=" + XDGRuntimeDir + "/sway" - // WaylandEnv is the environment variable string for the wayland display. - WaylandEnv = "WAYLAND_DISPLAY=wayland-1" -) - -// MustStartSway starts the sway wayland display server which must be terminated -// by calling [TerminateSway]. -func MustStartSway( - wg *sync.WaitGroup, - cred *syscall.Credential, - dbusEnv string, -) { - wg.Go(func() { - // this is terminated via swaymsg - _, done := MustStartWith( - context.Background(), cred, []string{ - "WLR_BACKENDS=headless", - XDGRuntimeEnv, - SwayEnv, - dbusEnv, - }, nil, - "sway", - ) - if err := <-done; err != nil { - log.Fatal(err) - } - }) - - Poll( - 50*time.Millisecond, - cred, - []string{SwayEnv}, - "swaymsg", - ) - log.Printf("sway available via %s", SwayEnv) -} - -// TerminateSway requests for the sway server to terminate via sway IPC. -func TerminateSway(cred *syscall.Credential) { - MustFail(cred, []string{SwayEnv}, "swaymsg", "exit") -} - -// MustStartPipeWire starts a PipeWire server that is never explicitly -// terminated. The test suite is terminated if the PipeWire server terminates. -func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) { - // this is never explicitly terminated - _, done := MustStartWith( - context.Background(), cred, []string{ - XDGRuntimeEnv, - dbusEnv, - }, nil, - "pipewire", - ) - - go func() { - if _err := <-done; _err != nil { - log.Fatal(_err) - } - log.Fatal("pipewire terminated unexpectedly") - }() - - Poll(50*time.Millisecond, cred, []string{ - XDGRuntimeEnv, - dbusEnv, - }, - "wpctl", - "status", - ) - - _, _done := MustStartWith( - context.Background(), cred, []string{ - XDGRuntimeEnv, - dbusEnv, - }, nil, - "wireplumber", - ) - - go func() { - if _err := <-_done; _err != nil { - log.Fatal(_err) - } - log.Fatal("wireplumber terminated unexpectedly") - }() -} diff --git a/test/internal/testsuite/testsuite_guard.go b/test/internal/testsuite/testsuite_guard.go deleted file mode 100644 index 0859d17f..00000000 --- a/test/internal/testsuite/testsuite_guard.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !testsuite && !tester - -package testsuite - -import ( - "os" - "testing" -) - -func init() { - if !testing.Testing() { - println("package testsuite imported in non-testsuite program") - os.Exit(1) - } -} diff --git a/test/internal/testsuite/testsuite_root.go b/test/internal/testsuite/testsuite_root.go deleted file mode 100644 index 11f503ef..00000000 --- a/test/internal/testsuite/testsuite_root.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build testsuite - -package testsuite - -import ( - "log" - "os" -) - -func init() { - if os.Geteuid() != 0 { - log.Fatal("this program must run as root") - } - - log.SetFlags(0) - log.SetPrefix("testsuite: ") -} diff --git a/test/sandbox/main.go b/test/sandbox/main.go deleted file mode 100644 index 4961c04f..00000000 --- a/test/sandbox/main.go +++ /dev/null @@ -1,409 +0,0 @@ -//go:build testsuite - -// The sandbox test program runs cmd/hakurei with configurations simulating -// several common workloads and inspects the resulting container states. -package main - -import ( - "bytes" - "context" - "encoding/json" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "slices" - "strconv" - "strings" - "sync" - "sync/atomic" - "syscall" - - "hakurei.app/check" - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/internal/store" - - "hakurei.app/test/internal/testsuite" - "hakurei.app/test/sandbox/testdata" -) - -// mustScanFor continuously scans the proc filesystem and calls f for each entry -// visited. -func mustScanFor(f func(ps *testsuite.StatScanner) bool) int { - var ps testsuite.StatScanner - - for ps.Scan() { - if f(&ps) { - break - } - } - if err := ps.Err(); err != nil { - log.Fatal(err) - } - return ps.Stat().PID -} - -// mustStart starts a hakurei container and returns the pid of a process within -// the container. This process must be terminated by the caller. -func mustStart( - ctx context.Context, - serial uint64, - cred *syscall.Credential, - files ...*os.File, -) (pid int, done <-chan error) { - _serial := strconv.FormatUint(serial, 10) - _, done = testsuite.MustStartWith( - ctx, cred, nil, files, - "hakurei", "exec", - "sleep", "infinity", _serial, - ) - - var stat syscall.Stat_t - pid = mustScanFor(func(s *testsuite.StatScanner) bool { - select { - case err := <-done: - if err == nil { - log.Fatal("test process terminated unexpectedly") - } - log.Fatal(err) - default: - break - } - - if s.Stat().Comm != "sleep" { - return false - } - - if args, err := s.Stat().Args(); err != nil { - if testsuite.IsNotExist(err) { - return false - } - log.Fatal(err) - } else if !slices.Equal(args, []string{ - "sleep", - "infinity", - _serial, - }) { - return false - } - - if err := s.Stat().Stat(&stat); err != nil { - if testsuite.IsNotExist(err) { - return false - } - log.Fatal(err) - } - - id := hst.ToUser[uint32](0, 0) - if stat.Uid != id || stat.Gid != id { - return false - } - - return true - }) - return -} - -func main() { - go testsuite.ReceiveSignals() - - // the signal handler does not wait for termination - ctx := context.Background() - - cred := syscall.Credential{Uid: 1000, Gid: 100} - if err := os.MkdirAll("/opt/test-helper/bin", 0755); err != nil { - log.Fatal(err) - } - - var testToolDone <-chan error - { - cmd := exec.Command( - "go", "build", - "-o", "/opt/test-helper/bin", - "-tags=tester", - "-trimpath", - "./test/sandbox/tester", - ) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - testToolDone = testsuite.MustStart(cmd) - } - - var wg sync.WaitGroup - defer wg.Wait() - - var serial atomic.Uint64 - newSerial := func() uint64 { serial.Add(1); return serial.Load() } - - testsuite.MustRun( - &cred, nil, - "hakurei", "exec", "capsh", "--print", - ) - wg.Go(func() { - defer log.Println("validated capabilities/securebits in user namespace") - - testsuite.MustRun( - &cred, nil, - "hakurei", "exec", "capsh", "--has-no-new-privs", - ) - - for _, p := range []byte{'a', 'b', 'i', 'p'} { - testsuite.MustFail( - &cred, nil, - "hakurei", "exec", "capsh", "--has-"+string(p)+"=CAP_SYS_ADMIN", - ) - } - testsuite.MustFail( - &cred, nil, - "hakurei", "exec", "umount", "-R", "/dev", - ) - }) - - wg.Go(func() { - defer log.Println("validated pd seccomp outcome") - - c, cancel := context.WithCancel(ctx) - defer cancel() - - pid, done := mustStart(c, newSerial(), &cred) - testsuite.MustCheckFilter(pid, testdata.SumPD) - if err := testsuite.FilterTerminated(<-done); err != nil { - log.Fatal(err) - } - }) - - wg.Go(func() { - defer log.Println("validated fd leak") - - c, cancel := context.WithCancel(ctx) - defer cancel() - - pid, done := mustStart(c, newSerial(), &cred, os.Stdin, os.Stdout, os.Stderr) - prefix := filepath.Join(fhs.Proc, strconv.Itoa(pid), "fd") - - var fail bool - if entries, err := os.ReadDir(prefix); err != nil { - log.Fatal(err.Error()) - } else { - for _, ent := range entries { - var fd int - if fd, err = strconv.Atoi(ent.Name()); err != nil { - log.Fatal(err.Error()) - } - - // skip standard streams - if fd <= 2 { - continue - } - fail = true - - var d string - if d, err = os.Readlink(filepath.Join( - prefix, - ent.Name(), - )); err != nil { - log.Fatal(err.Error()) - } - log.Printf("extra fd %d -> %s", fd, d) - } - } - if fail { - log.Fatal("file descriptors leaked") - } - - if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { - log.Fatalf("cannot terminate anchor: %v", err) - } else if err = testsuite.FilterTerminated(<-done); err != nil { - log.Fatal(err) - } - }) - - if err := os.MkdirAll(testsuite.XDGRuntimeDir, 0700); err != nil { - log.Fatal(err) - } else if err = os.Chown(testsuite.XDGRuntimeDir, 1000, 1000); err != nil { - log.Fatal(err) - } - - var swg sync.WaitGroup - defer swg.Wait() - dbusEnv := testsuite.MustStartSessionBus(&cred) - testsuite.MustStartSway(&swg, &cred, dbusEnv) - defer testsuite.TerminateSway(&cred) - testsuite.MustStartPipeWire(&cred, dbusEnv) - - if err := <-testToolDone; err != nil { - log.Fatal(err) - } - log.Println("created test helper") - - s := store.New(check.MustAbs("/tmp/hakurei.0/state")) - for name, tc := range testdata.All() { - wg.Go(func() { - cmd := exec.Command( - "script", "/dev/null", - "-E", "always", - "-qec", - "hakurei run "+ - "--identifier-fd=5"+ - " 4 1>&3", - ) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - Credential: &cred, - } - var output bytes.Buffer - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &output, &output - cmd.Env = []string{ - "PATH=" + os.Getenv("PATH"), - "TERM=xterm", - testsuite.XDGRuntimeEnv, - testsuite.WaylandEnv, - "DISPLAY=:0", - dbusEnv, - } - - var err error - var notify, _notify, _conf, conf, ident, _ident *os.File - if notify, _notify, err = os.Pipe(); err != nil { - log.Fatal(err) - } - cmd.ExtraFiles = append(cmd.ExtraFiles, _notify) - if _conf, conf, err = os.Pipe(); err != nil { - log.Fatal(err) - } - cmd.ExtraFiles = append(cmd.ExtraFiles, _conf) - if ident, _ident, err = os.Pipe(); err != nil { - log.Fatal(err) - } - cmd.ExtraFiles = append(cmd.ExtraFiles, _ident) - - done := testsuite.MustStart(cmd) - wg.Go(func() { - _err := <-done - log.Printf("completed test case %s\n%s", name, output.String()) - if _err != nil { - log.Fatalf("test case %s: %v", name, _err) - } - }) - - if err = json.NewEncoder(conf).Encode(&tc.Hakurei); err != nil { - log.Fatal(err) - } else if err = conf.Close(); err != nil { - log.Fatal(err) - } - - var id hst.ID - if _, err = io.ReadFull(ident, id[:]); err != nil { - log.Fatal(err) - } else if err = ident.Close(); err != nil { - log.Fatal(err) - } - - if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil { - log.Fatal(err) - } else if err = notify.Close(); err != nil { - log.Fatal(err) - } - - var ( - ok bool - p hst.State - ) - entries, copyError := s.All() - for entry := range entries { - if entry.ID == id { - ok = true - if _, err = entry.Load(&p, nil); err != nil { - log.Fatal(err) - } - break - } - } - if err = copyError(); err != nil { - log.Fatal(err) - } - if !ok { - log.Fatalf("instance %s is not present in store", id) - } - - var stat syscall.Stat_t - pid := mustScanFor(func(ps *testsuite.StatScanner) bool { - select { - case err = <-done: - if err == nil { - log.Fatal("test process terminated unexpectedly") - } - log.Fatal(err) - default: - break - } - - if ps.Stat().Comm != "test-helper" { - return false - } - - var args []string - if args, err = ps.Stat().Args(); err != nil { - if testsuite.IsNotExist(err) { - return false - } - log.Fatal(err) - } else if !slices.Equal(args, tc.Hakurei.Container.Args) { - return false - } - - if err = ps.Stat().Stat(&stat); err != nil { - if testsuite.IsNotExist(err) { - return false - } - log.Fatal(err) - } - - uid := hst.ToUser[uint32](0, uint32(tc.Hakurei.Identity)) - if stat.Uid != uid || stat.Gid != uid { - return false - } - - var t []byte - if t, err = os.ReadFile(filepath.Join( - fhs.Proc, - strconv.Itoa(ps.Stat().PPID), - "stat", - )); err != nil { - if testsuite.IsNotExist(err) { - return false - } - log.Fatal(err) - } - - var _stat testsuite.Stat - if err = _stat.UnmarshalText(t); err != nil { - log.Fatal(err) - } - if _stat.PPID != p.ShimPID { - return false - } - - return true - }) - - testsuite.MustCheckFilter( - pid, - tc.Sum, - ) - }) - } - - wg.Wait() - - if dents, err := os.ReadDir("/tmp"); err != nil { - log.Fatal(err) - } else { - for _, dent := range dents { - if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") { - log.Fatalf("leftover shim work dir %q", name) - } - } - } -} diff --git a/test/sandbox/seccomp.patch b/test/sandbox/seccomp.patch deleted file mode 100644 index ddabc71e..00000000 --- a/test/sandbox/seccomp.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/kernel/seccomp.c b/kernel/seccomp.c -index 25f62867a16d..7b63ccc8daf4 100644 ---- a/kernel/seccomp.c -+++ b/kernel/seccomp.c -@@ -2216,8 +2216,12 @@ long seccomp_get_filter(struct task_struct *task, unsigned long filter_off, - struct seccomp_filter *filter; - struct sock_fprog_kern *fprog; - long ret; -+ struct user_namespace *user_ns = current_user_ns(); - -- if (!capable(CAP_SYS_ADMIN) || -+ if (in_userns(user_ns, task_cred_xxx(task, user_ns))) { -+ if (!ns_capable(user_ns, CAP_SYS_ADMIN)) -+ return -EACCES; -+ } else if (!capable(CAP_SYS_ADMIN) || - current->seccomp.mode != SECCOMP_MODE_DISABLED) { - return -EACCES; - } diff --git a/test/sandbox/testdata/device.go b/test/sandbox/testdata/device.go deleted file mode 100644 index 89feb819..00000000 --- a/test/sandbox/testdata/device.go +++ /dev/null @@ -1,134 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "os" - "syscall" - - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -var _ = TestCase{ - Hakurei: hst.Config{ - ID: "app.hakurei.sample.device", - Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus | hst.EX11), - Identity: 4, - - Container: &hst.ContainerConfig{ - Hostname: "hakurei-sample-device", - - Filesystem: []hst.FilesystemConfigJSON{ - fcLinker, - fcLib, - fcTestHelper, - }, - - Username: "u0_a4", - Shell: fhs.AbsUsrBin.Append("bash"), - Home: hst.AbsPrivateTmp, - Path: absTestHelper, - Args: []string{"tester", "device"}, - - Flags: hst.FDevice | hst.FShareTmpdir, - }, - }, - - // 0, PresetStrict - Sum: sumSimple, - - Env: []string{ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", - "DISPLAY=unix:/tmp/.X11-unix/X0", - "HOME=/.hakurei", - "SHELL=/usr/bin/bash", - "TERM=xterm", - "USER=u0_a4", - "WAYLAND_DISPLAY=wayland-0", - "XDG_RUNTIME_DIR=/run/user/65534", - "XDG_SESSION_CLASS=user", - "XDG_SESSION_TYPE=wayland", - "PULSE_SERVER=unix:/run/user/65534/pulse/native", - }, - - FS: &testsuite.FS{Dir: dir{ - ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ - "test-helper": {Mode: 0755}, - }}, - - // unstable host dev - "dev": {Mode: os.ModeDir | 0755}, - - "etc": {Mode: os.ModeDir | 0755, Dir: dir{ - "passwd": {Mode: 0600, - Data: new("u0_a4:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, - "group": {Mode: 0600, - Data: new("hakurei:x:65534:\n")}, - }}, - - "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ - "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, - }}, - - "run": {Mode: os.ModeDir | 0755, Dir: dir{ - "user": {Mode: os.ModeDir | 0755, Dir: dir{ - "65534": {Mode: os.ModeDir | 0700, Dir: dir{ - "bus": {Mode: os.ModeSocket | 0775}, - "wayland-0": {Mode: os.ModeSocket | 070}, - "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ - "native": {Mode: os.ModeSocket | 0777}, - }}, - }}, - }}, - }}, - - "tmp": {Mode: os.ModeDir | 0770, Dir: dir{ - ".X11-unix": {Mode: os.ModeDir | 0755, Dir: dir{ - "X0": {Mode: os.ModeSocket | 0775}, - }}, - }}, - - "lib": {Mode: os.ModeDir | 0755}, - "proc": {Mode: os.ModeDir | 0555}, - }}, - - Mount: []*mountinfo.Entry{ - r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), - r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), - r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110003,gid=110003,inode64"), - - // host /dev in testing environment - r("/", "/dev", "rw,nosuid", "tmpfs", "tmpfs", ignore), - r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,gid=100004,mode=620,ptmxmode=666"), - r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - r("/kvm", "/dev/kvm", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/fuse", "/dev/fuse", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/", "/dev/shm", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "shm", ignore), - r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - - r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110003,gid=110003,inode64"), - r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110003,gid=110003,inode64"), - r("/tmp/hakurei.0/tmpdir/4", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), - r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), - r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/tmp/.X11-unix", "/tmp/.X11-unix", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - }, - - Seccomp: true, - - TrySocket: "/tmp/.X11-unix/X0", - ErrnoAbstract: syscall.ECONNREFUSED, -}.register("device") diff --git a/test/sandbox/testdata/mapuid.go b/test/sandbox/testdata/mapuid.go deleted file mode 100644 index 8dee7b7f..00000000 --- a/test/sandbox/testdata/mapuid.go +++ /dev/null @@ -1,124 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "os" - "syscall" - - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -var _ = TestCase{ - Hakurei: hst.Config{ - ID: "app.hakurei.sample.mapuid", - Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), - Identity: 3, - - Container: &hst.ContainerConfig{ - Hostname: "hakurei-sample-mapuid", - - Filesystem: []hst.FilesystemConfigJSON{ - fcLinker, - fcLib, - fcTestHelper, - }, - - Username: "u0_a3", - Shell: fhs.AbsUsrBin.Append("bash"), - Home: hst.AbsPrivateTmp, - Path: absTestHelper, - Args: []string{"tester", "mapuid"}, - - Flags: hst.FMapRealUID | hst.FShareRuntime | hst.FShareTmpdir, - }, - }, - - // 0, PresetStrict - Sum: sumSimple, - - Env: []string{ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus", - "HOME=/.hakurei", - "SHELL=/usr/bin/bash", - "TERM=xterm", - "USER=u0_a3", - "WAYLAND_DISPLAY=wayland-0", - "XDG_RUNTIME_DIR=/run/user/1000", - "XDG_SESSION_CLASS=user", - "XDG_SESSION_TYPE=wayland", - "PULSE_SERVER=unix:/run/user/1000/pulse/native", - }, - - FS: &testsuite.FS{Dir: dir{ - ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ - "test-helper": {Mode: 0755}, - }}, - - // unstable host dev - "dev": {Mode: os.ModeDir | 0755}, - - "etc": {Mode: os.ModeDir | 0755, Dir: dir{ - "passwd": {Mode: 0600, - Data: new("u0_a3:x:1000:100:Hakurei:/.hakurei:/usr/bin/bash\n")}, - "group": {Mode: 0600, - Data: new("hakurei:x:100:\n")}, - }}, - - "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ - "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, - }}, - - "run": {Mode: os.ModeDir | 0755, Dir: dir{ - "user": {Mode: os.ModeDir | 0755, Dir: dir{ - "1000": {Mode: os.ModeDir | 0770, Dir: dir{ - "bus": {Mode: os.ModeSocket | 0775}, - "wayland-0": {Mode: os.ModeSocket | 070}, - "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ - "native": {Mode: os.ModeSocket | 0777}, - }}, - }}, - }}, - }}, - - "tmp": {Mode: os.ModeDir | 0770, Dir: dir{}}, - - "lib": {Mode: os.ModeDir | 0755}, - "proc": {Mode: os.ModeDir | 0555}, - }}, - - Mount: []*mountinfo.Entry{ - r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), - r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), - r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110002,gid=110002,inode64"), - r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110002,gid=110002,inode64"), - r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), - r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110002,gid=110002,inode64"), - r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110002,gid=110002,inode64"), - r("/tmp/hakurei.0/runtime/3", "/run/user/1000", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/tmp/hakurei.0/tmpdir/3", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), - r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), - r(ignore, "/run/user/1000/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/1000/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/1000/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - }, - - Seccomp: true, - - TrySocket: "/tmp/.X11-unix/X0", - ErrnoAbstract: syscall.ECONNREFUSED, - ErrnoPathname: syscall.ENOENT, -}.register("mapuid") diff --git a/test/sandbox/testdata/pdlike.go b/test/sandbox/testdata/pdlike.go deleted file mode 100644 index 53d8062a..00000000 --- a/test/sandbox/testdata/pdlike.go +++ /dev/null @@ -1,141 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "os" - "syscall" - - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -var _ = TestCase{ - Hakurei: hst.Config{ - ID: "app.hakurei.sample.pdlike", - Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), - Identity: 5, - - Container: &hst.ContainerConfig{ - Hostname: "hakurei-sample-pdlike", - - Filesystem: []hst.FilesystemConfigJSON{ - fcLinker, - fcLib, - fcTestHelper, - }, - - Username: "u0_a5", - Shell: fhs.AbsUsrBin.Append("bash"), - Home: hst.AbsPrivateTmp, - Path: absTestHelper, - Args: []string{"tester", "pdlike"}, - - Flags: hst.FHostNet | hst.FTty | hst.FUserns | hst.FShareRuntime | hst.FShareTmpdir, - }, - }, - - // 0, PresetExt | PresetDenyDevel - Sum: SumPD, - - Env: []string{ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", - "HOME=/.hakurei", - "SHELL=/usr/bin/bash", - "TERM=xterm", - "USER=u0_a5", - "WAYLAND_DISPLAY=wayland-0", - "XDG_RUNTIME_DIR=/run/user/65534", - "XDG_SESSION_CLASS=user", - "XDG_SESSION_TYPE=wayland", - "PULSE_SERVER=unix:/run/user/65534/pulse/native", - }, - - FS: &testsuite.FS{Dir: dir{ - ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ - "test-helper": {Mode: 0755}, - }}, - - "dev": {Mode: os.ModeDir | 0755, Dir: dir{ - "core": {Mode: os.ModeSymlink | 0777}, - "fd": {Mode: os.ModeSymlink | 0777}, - "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, - "ptmx": {Mode: os.ModeSymlink | 0777}, - "pts": {Mode: os.ModeDir | 0755, Dir: dir{ - "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "stderr": {Mode: os.ModeSymlink | 0777}, - "stdin": {Mode: os.ModeSymlink | 0777}, - "stdout": {Mode: os.ModeSymlink | 0777}, - "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, - "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - - "etc": {Mode: os.ModeDir | 0755, Dir: dir{ - "passwd": {Mode: 0600, - Data: new("u0_a5:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, - "group": {Mode: 0600, - Data: new("hakurei:x:65534:\n")}, - }}, - - "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ - "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, - }}, - - "run": {Mode: os.ModeDir | 0755, Dir: dir{ - "user": {Mode: os.ModeDir | 0755, Dir: dir{ - "65534": {Mode: os.ModeDir | 0770, Dir: dir{ - "bus": {Mode: os.ModeSocket | 0775}, - "wayland-0": {Mode: os.ModeSocket | 070}, - "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ - "native": {Mode: os.ModeSocket | 0777}, - }}, - }}, - }}, - }}, - - "tmp": {Mode: os.ModeDir | 0770, Dir: dir{}}, - - "lib": {Mode: os.ModeDir | 0755}, - "proc": {Mode: os.ModeDir | 0555}, - }}, - - Mount: []*mountinfo.Entry{ - r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), - r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), - r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110004,gid=110004,inode64"), - r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110004,gid=110004,inode64"), - r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), - r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110004,gid=110004,inode64"), - r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110004,gid=110004,inode64"), - r("/tmp/hakurei.0/runtime/5", "/run/user/65534", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/tmp/hakurei.0/tmpdir/5", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), - r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), - r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - }, - - Seccomp: true, - - TrySocket: "/tmp/.X11-unix/X0", - ErrnoAbstract: syscall.EPERM, - ErrnoPathname: syscall.ENOENT, -}.register("pdlike") diff --git a/test/sandbox/testdata/simple.go b/test/sandbox/testdata/simple.go deleted file mode 100644 index c410e626..00000000 --- a/test/sandbox/testdata/simple.go +++ /dev/null @@ -1,140 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "os" - "syscall" - - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -var _ = TestCase{ - Hakurei: hst.Config{ - ID: "app.hakurei.sample.simple", - Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), - Identity: 1, - - Container: &hst.ContainerConfig{ - Hostname: "hakurei-sample-simple", - Env: map[string]string{"HAKUREI_SAMPLE": "1"}, - - Filesystem: []hst.FilesystemConfigJSON{ - fcLinker, - fcLib, - fcTestHelper, - }, - - Username: "u0_a1", - Shell: fhs.AbsUsrBin.Append("bash"), - Home: hst.AbsPrivateTmp, - Path: absTestHelper, - Args: []string{"tester", "simple"}, - }, - }, - - // 0, PresetStrict - Sum: sumSimple, - - Env: []string{ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", - "HAKUREI_SAMPLE=1", - "HOME=/.hakurei", - "SHELL=/usr/bin/bash", - "TERM=xterm", - "USER=u0_a1", - "WAYLAND_DISPLAY=wayland-0", - "XDG_RUNTIME_DIR=/run/user/65534", - "XDG_SESSION_CLASS=user", - "XDG_SESSION_TYPE=wayland", - "PULSE_SERVER=unix:/run/user/65534/pulse/native", - }, - - FS: &testsuite.FS{Dir: dir{ - ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ - "test-helper": {Mode: 0755}, - }}, - - "dev": {Mode: os.ModeDir | 0755, Dir: dir{ - "core": {Mode: os.ModeSymlink | 0777}, - "fd": {Mode: os.ModeSymlink | 0777}, - "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, - "ptmx": {Mode: os.ModeSymlink | 0777}, - "pts": {Mode: os.ModeDir | 0755, Dir: dir{ - "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "stderr": {Mode: os.ModeSymlink | 0777}, - "stdin": {Mode: os.ModeSymlink | 0777}, - "stdout": {Mode: os.ModeSymlink | 0777}, - "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, - "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - - "etc": {Mode: os.ModeDir | 0755, Dir: dir{ - "passwd": {Mode: 0600, - Data: new("u0_a1:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, - "group": {Mode: 0600, - Data: new("hakurei:x:65534:\n")}, - }}, - - "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ - "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, - }}, - - "run": {Mode: os.ModeDir | 0755, Dir: dir{ - "user": {Mode: os.ModeDir | 0755, Dir: dir{ - "65534": {Mode: os.ModeDir | 0700, Dir: dir{ - "bus": {Mode: os.ModeSocket | 0775}, - "wayland-0": {Mode: os.ModeSocket | 070}, - "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ - "native": {Mode: os.ModeSocket | 0777}, - }}, - }}, - }}, - }}, - - "tmp": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - - "lib": {Mode: os.ModeDir | 0755}, - "proc": {Mode: os.ModeDir | 0555}, - }}, - - Mount: []*mountinfo.Entry{ - r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), - r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), - r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110000,gid=110000,inode64"), - r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110000,gid=110000,inode64"), - r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), - r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110000,gid=110000,inode64"), - r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110000,gid=110000,inode64"), - r("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110000,gid=110000,inode64"), - r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), - r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), - r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - }, - - Seccomp: true, - - TrySocket: "/tmp/.X11-unix/X0", - ErrnoAbstract: syscall.ECONNREFUSED, - ErrnoPathname: syscall.ENOENT, -}.register("simple") diff --git a/test/sandbox/testdata/sum.go b/test/sandbox/testdata/sum.go deleted file mode 100644 index e4e8643a..00000000 --- a/test/sandbox/testdata/sum.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "crypto/sha512" - "encoding/base64" - "strconv" -) - -// sum decodes s as [base64.StdEncoding] and panics if it is invalid or -// unexpectedly sized. -func sum(s string) [sha512.Size]byte { - p, err := base64.StdEncoding.DecodeString(s) - if err != nil { - panic(err) - } - if len(p) != sha512.Size { - panic("unexpected checksum sized " + strconv.Itoa(len(p))) - } - return ([sha512.Size]byte)(p) -} diff --git a/test/sandbox/testdata/sum_amd64.go b/test/sandbox/testdata/sum_amd64.go deleted file mode 100644 index bd751105..00000000 --- a/test/sandbox/testdata/sum_amd64.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build testsuite || tester - -package testdata - -var ( - SumPD = sum("xpiwgf+Vev4XptlDdFN9N/KmP2+d112nVGVCQHqeMkduvaMxK6d4XX9hhUK8+vJ8on3MLd26hSBp0ovP6MrTmg==") - sumSimple = sum("6IApjfK9Z1HQBA/CG8DtTAD5XcDXulBsJE2LjPaGbbqO9KMylvKHtmzMwdeOlwJll/hMx97BVz4UiWD701zXNQ==") - sumTTY = sum("C3YAdHbByeJdv2dMKf32CaFlanAGPkkydlThtTYK09oG4aPjK/gOlhxVFq2D1Lnn6b3odqk3l+J2J9JVXCWFiw==") -) diff --git a/test/sandbox/testdata/sum_arm64.go b/test/sandbox/testdata/sum_arm64.go deleted file mode 100644 index 1691828f..00000000 --- a/test/sandbox/testdata/sum_arm64.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build testsuite || tester - -package testdata - -var ( - SumPD = sum("QzzpuREoLW3MgCkxn7ebgWtg1aeV7I/JQ0TdAnYU1o8CMWapG7iB+q7u3Sbj2JR04UHlppqX6TuJhMqPFJmZgA==") - sumSimple = sum("eTGFOKPchRMUtr2W8Q1YYayyqn4Ty43gYZ0PanZwnWfwHvP9Z+GVhisC+XEeW3abxNHrT8DfxBpyPInJaKkylw==") - sumTTY = sum("zx9NyHQ2uo7JXSaLZjpjl7sLSlrGTYVX5sxSnYsPb2Xa06krYu0p2F7unG3eEmd1ek0PhgMuikXKG86t+jTPXg==") -) diff --git a/test/sandbox/testdata/testdata.go b/test/sandbox/testdata/testdata.go deleted file mode 100644 index 3418d8ae..00000000 --- a/test/sandbox/testdata/testdata.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build testsuite || tester - -// Package testdata holds sandbox inspection test cases. -package testdata - -import ( - "crypto/sha512" - "iter" - "log" - "strconv" - "syscall" - - "hakurei.app/check" - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -// A TestCase represents a named test case that may be requested by the caller. -type TestCase struct { - // Configuration of the inspected container. - Hakurei hst.Config - // Checksum of expected seccomp filter program. - Sum [sha512.Size]byte - - // Expected environment. Skipped if nil. - Env []string `json:"env,omitempty"` - // Expected root filesystem. Skipped if nil. - FS *testsuite.FS `json:"fs,omitempty"` - // Expected mountinfo records. Skipped if nil. - Mount []*mountinfo.Entry `json:"mount,omitempty"` - // Whether to run seccomp checks. - Seccomp bool `json:"seccomp,omitempty"` - - // Name of pathname and abstract sockets to attempt. - TrySocket string `json:"try_socket,omitempty"` - // Errno to expect attempting to reach the abstract socket. - ErrnoAbstract syscall.Errno `json:"errno_abstract,omitempty"` - // Errno to expect attempting to reach the pathname socket. - ErrnoPathname syscall.Errno `json:"errno_pathname,omitempty"` -} - -// testCases hold all named test cases. -var testCases map[string]TestCase - -// fc returns c wrapped in its JSON adapter. -func fc(c hst.FilesystemConfig) hst.FilesystemConfigJSON { - return hst.FilesystemConfigJSON{ - FilesystemConfig: c, - } -} - -// ignore is the magic string for a mountinfo field to be ignored. -const ignore = "//ignore" - -type dir = map[string]*testsuite.FS - -// r returns the address of a [mountinfo.Entry]. -func r( - root, target, vfsOptstr string, - fsType, source, fsOptstr string, -) *mountinfo.Entry { - return &mountinfo.Entry{ - ID: -1, - Parent: -1, - Root: root, - Target: target, - VfsOptstr: vfsOptstr, - FsType: fsType, - Source: source, - FsOptstr: fsOptstr, - } -} - -var ( - // fcLinker is the dynamic linker symlink. - fcLinker = fc(&hst.FSLink{ - Target: fhs.AbsRoot.Append("lib64", "ld-linux-x86-64.so.2"), - Linkname: "../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", - }) - // fcLib is the dynamic library bind mount. - fcLib = fc(&hst.FSBind{Source: fhs.AbsRoot.Append("lib")}) - - // absTestHelper is the absolute pathname of the test helper program. - absTestHelper = hst.AbsPrivateTmp.Append("test-helper") - // fcTestHelper is the test helper bind mount. - fcTestHelper = fc(&hst.FSBind{ - Target: absTestHelper, - Source: check.MustAbs("/opt/test-helper/bin/tester"), - }) -) - -// register adds a test case to testCases. -func (c TestCase) register(name string) (_ struct{}) { - if testCases == nil { - testCases = make(map[string]TestCase) - } - - if _, ok := testCases[name]; ok { - panic("attempting to register " + strconv.Quote(name) + " twice") - } - testCases[name] = c - return -} - -// Get returns the named test case, or terminates the program if name is invalid. -func Get(name string) TestCase { - tc, ok := testCases[name] - if !ok { - log.Fatalf("invalid test case %q", name) - } - return tc -} - -// All returns an iterator over all named test cases. -func All() iter.Seq2[string, TestCase] { - return func(yield func(string, TestCase) bool) { - for name, tc := range testCases { - if !yield(name, tc) { - return - } - } - } -} diff --git a/test/sandbox/testdata/tty.go b/test/sandbox/testdata/tty.go deleted file mode 100644 index 4788f484..00000000 --- a/test/sandbox/testdata/tty.go +++ /dev/null @@ -1,145 +0,0 @@ -//go:build testsuite || tester - -package testdata - -import ( - "os" - - "hakurei.app/fhs" - "hakurei.app/hst" - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/internal/testsuite" -) - -var _ = TestCase{ - Hakurei: hst.Config{ - ID: "app.hakurei.sample.tty", - Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus | hst.EX11), - Identity: 2, - - Container: &hst.ContainerConfig{ - Hostname: "hakurei-sample-tty", - - Filesystem: []hst.FilesystemConfigJSON{ - fcLinker, - fcLib, - fcTestHelper, - }, - - Username: "u0_a2", - Shell: fhs.AbsUsrBin.Append("bash"), - Home: hst.AbsPrivateTmp, - Path: absTestHelper, - Args: []string{"tester", "tty"}, - - Flags: hst.FHostNet | hst.FHostAbstract | - hst.FTty | hst.FShareRuntime, - }, - }, - - // 0, PresetExt | PresetDenyNS | PresetDenyDevel - Sum: sumTTY, - - Env: []string{ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", - "DISPLAY=:0", - "HOME=/.hakurei", - "SHELL=/usr/bin/bash", - "TERM=xterm", - "USER=u0_a2", - "WAYLAND_DISPLAY=wayland-0", - "XDG_RUNTIME_DIR=/run/user/65534", - "XDG_SESSION_CLASS=user", - "XDG_SESSION_TYPE=wayland", - "PULSE_SERVER=unix:/run/user/65534/pulse/native", - }, - - FS: &testsuite.FS{Dir: dir{ - ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ - "test-helper": {Mode: 0755}, - }}, - - "dev": {Mode: os.ModeDir | 0755, Dir: dir{ - "core": {Mode: os.ModeSymlink | 0777}, - "fd": {Mode: os.ModeSymlink | 0777}, - "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, - "ptmx": {Mode: os.ModeSymlink | 0777}, - "pts": {Mode: os.ModeDir | 0755, Dir: dir{ - "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, - "stderr": {Mode: os.ModeSymlink | 0777}, - "stdin": {Mode: os.ModeSymlink | 0777}, - "stdout": {Mode: os.ModeSymlink | 0777}, - "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, - "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, - }}, - - "etc": {Mode: os.ModeDir | 0755, Dir: dir{ - "passwd": {Mode: 0600, - Data: new("u0_a2:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, - "group": {Mode: 0600, - Data: new("hakurei:x:65534:\n")}, - }}, - - "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ - "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, - }}, - - "run": {Mode: os.ModeDir | 0755, Dir: dir{ - "user": {Mode: os.ModeDir | 0755, Dir: dir{ - "65534": {Mode: os.ModeDir | 0770, Dir: dir{ - "bus": {Mode: os.ModeSocket | 0775}, - "wayland-0": {Mode: os.ModeSocket | 070}, - "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ - "native": {Mode: os.ModeSocket | 0777}, - }}, - }}, - }}, - }}, - - "tmp": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{ - ".X11-unix": {Mode: os.ModeDir | 0755, Dir: dir{ - "X0": {Mode: os.ModeSocket | 0775}, - }}, - }}, - - "lib": {Mode: os.ModeDir | 0755}, - "proc": {Mode: os.ModeDir | 0555}, - }}, - - Mount: []*mountinfo.Entry{ - r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), - r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), - r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110001,gid=110001,inode64"), - r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110001,gid=110001,inode64"), - r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), - r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), - r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110001,gid=110001,inode64"), - r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110001,gid=110001,inode64"), - r("/tmp/hakurei.0/runtime/2", "/run/user/65534", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110001,gid=110001,inode64"), - r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), - r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), - r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/tmp/.X11-unix", "/tmp/.X11-unix", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), - }, - - Seccomp: true, - - TrySocket: "/tmp/.X11-unix/X0", -}.register("tty") diff --git a/test/sandbox/tester/main.go b/test/sandbox/tester/main.go deleted file mode 100644 index 0782a5e0..00000000 --- a/test/sandbox/tester/main.go +++ /dev/null @@ -1,224 +0,0 @@ -//go:build tester - -// The sandbox tester runs within a cmd/hakurei container and validates its -// state. Since the test environment is relatively predictable, the tester can -// make various assumptions about the host. -package main - -import ( - "errors" - "log" - "net" - "os" - "os/signal" - "path/filepath" - "syscall" - - "hakurei.app/test/internal/mountinfo" - "hakurei.app/test/sandbox/testdata" -) - -//#include <sys/quota.h> -import "C" - -// mustAbs returns s, or terminates the program if s is not absolute. -func mustAbs(s string) string { - if !filepath.IsAbs(s) { - log.Fatalf("%q is not absolute", s) - } - return s -} - -func main() { - log.SetFlags(0) - log.SetPrefix("tester: ") - - if len(os.Args) != 2 { - log.Fatal("tester requires 1 argument") - } - want := testdata.Get(os.Args[1]) - log.SetPrefix("tester: " + os.Args[1] + " ") - - checkWritableDirPaths := []string{ - "/dev/shm", - "/tmp", - os.Getenv("XDG_RUNTIME_DIR"), - } - for _, a := range checkWritableDirPaths { - pathname := filepath.Join(mustAbs(a), ".hakurei-check") - if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil { - log.Fatalf("[FAIL] %s", err) - } else if err = os.Remove(pathname); err != nil { - log.Fatalf("[FAIL] %s", err) - } else { - log.Printf("[ OK ] %s is writable", a) - } - } - - if want.Env != nil { - var ( - fail bool - i int - got string - ) - for i, got = range os.Environ() { - if i == len(want.Env) { - log.Fatalf("got more than %d environment variables", len(want.Env)) - } - if got != want.Env[i] { - fail = true - log.Printf("[FAIL] %s", got) - } else { - log.Printf("[ OK ] %s", got) - } - } - - i++ - if i != len(want.Env) { - log.Fatalf("got %d environment variables, want %d", i, len(want.Env)) - } - - if fail { - log.Fatalf("[FAIL] some environment variables did not match") - } - } else { - log.Printf("[SKIP] skipping environ check") - } - - if want.FS != nil { - if err := want.FS.Compare(log.Printf, ".", os.DirFS("/")); err != nil { - log.Fatalf("%v", err) - } - } else { - log.Printf("[SKIP] skipping fs check") - } - - if want.Mount != nil { - var fail bool - - m, err := mountinfo.Open("") - if err != nil { - log.Fatal(err) - } - - i := 0 - var ent mountinfo.Entry - for m.Next() { - m.Copy(&ent) - - if i == len(want.Mount) { - log.Fatalf("got more than %d entries", i) - } - if !ent.EqualWithIgnore(want.Mount[i], "//ignore") { - fail = true - log.Printf("[FAIL] %s", &ent) - } else { - log.Printf("[ OK ] %s", &ent) - } - - i++ - } - if err = m.Err(); err != nil { - log.Fatalf("%v", err) - } - - if i != len(want.Mount) { - log.Fatalf("got %d entries, want %d", i, len(want.Mount)) - } - - if fail { - log.Fatalf("[FAIL] some mount points did not match") - } - } else { - log.Printf("[SKIP] skipping mounts check") - } - - if want.Seccomp { - const NULL = 0 - - for _, tc := range []struct { - name string - errno syscall.Errno - - trap, a1, a2, a3, a4, a5, a6 uintptr - }{ - {"syslog", syscall.EPERM, syscall.SYS_SYSLOG, 0, NULL, NULL, NULL, NULL, NULL}, - {"acct", syscall.EPERM, syscall.SYS_ACCT, 0, NULL, NULL, NULL, NULL, NULL}, - {"quotactl", syscall.EPERM, syscall.SYS_QUOTACTL, C.Q_GETQUOTA, NULL, uintptr(os.Getuid()), NULL, NULL, NULL}, - {"add_key", syscall.EPERM, syscall.SYS_ADD_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"keyctl", syscall.EPERM, syscall.SYS_KEYCTL, NULL, NULL, NULL, NULL, NULL, NULL}, - {"request_key", syscall.EPERM, syscall.SYS_REQUEST_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"move_pages", syscall.EPERM, syscall.SYS_MOVE_PAGES, uintptr(os.Getpid()), NULL, NULL, NULL, NULL, NULL}, - {"mbind", syscall.EPERM, syscall.SYS_MBIND, NULL, NULL, NULL, NULL, NULL, NULL}, - {"get_mempolicy", syscall.EPERM, syscall.SYS_GET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"set_mempolicy", syscall.EPERM, syscall.SYS_SET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"migrate_pages", syscall.EPERM, syscall.SYS_MIGRATE_PAGES, NULL, NULL, NULL, NULL, NULL, NULL}, - } { - if _, _, errno := syscall.Syscall6(tc.trap, tc.a1, tc.a2, tc.a3, tc.a4, tc.a5, tc.a6); errno != tc.errno { - log.Fatalf("[FAIL] %s: %v, want %v", tc.name, errno, tc.errno) - } - log.Printf("[ OK ] %s: %v", tc.name, tc.errno) - } - } else { - log.Printf("[SKIP] skipping seccomp check") - } - - if want.TrySocket != "" { - retry: - abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket) - pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket) - ok := true - - if abstractErr == nil { - if err := abstractConn.Close(); err != nil { - ok = false - log.Printf("Close: %v", err) - } - } - if pathnameErr == nil { - if err := pathnameConn.Close(); err != nil { - ok = false - log.Printf("Close: %v", err) - } - } - - if errors.Is( - abstractErr, - syscall.EAGAIN, - ) || errors.Is( - pathnameErr, - syscall.EAGAIN, - ) { - goto retry - } - - abstractWantErr := error(want.ErrnoAbstract) - pathnameWantErr := error(want.ErrnoPathname) - if want.ErrnoAbstract == 0 { - abstractWantErr = nil - } - if want.ErrnoPathname == 0 { - pathnameWantErr = nil - } - - if !errors.Is(abstractErr, abstractWantErr) { - ok = false - log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr) - } - if !errors.Is(pathnameErr, pathnameWantErr) { - ok = false - log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr) - } - - if !ok { - os.Exit(1) - } - } - - s := make(chan os.Signal, 1) - signal.Notify(s, syscall.SIGTERM) - if _, err := os.Stdout.Write(make([]byte, 8)); err != nil { - log.Fatalf("cannot notify testsuite: %v", err) - } - <-s -} diff --git a/test/sharefs/main.go b/test/sharefs/main.go deleted file mode 100644 index 80e6ab09..00000000 --- a/test/sharefs/main.go +++ /dev/null @@ -1,119 +0,0 @@ -//go:build testsuite - -// The sharefs test program checks cli behaviour and exercises the filesystem -// implemented by cmd/sharefs using fs_mark. -package main - -import ( - "errors" - "log" - "os" - "os/exec" - "strings" - "syscall" - - "hakurei.app/test/internal/testsuite" -) - -// checkBadOpts invokes cmd/sharefs with the specified options and compares -// the resulting error message. -func checkBadOpts(cred *syscall.Credential, opts, want string) { - var buf strings.Builder - buf.Grow(len(want)) - - cmd := exec.Command( - "sharefs", - "-f", - "-o", "source=/etc,"+opts, - "/mnt", - ) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGKILL, - Credential: cred, - } - cmd.Stderr = &buf - err := cmd.Run() - if err == nil { - log.Fatalf("opts=%q, unexpected success", opts) - } - if e, ok := errors.AsType[*exec.ExitError](err); !ok { - log.Fatal(err) - } else if !e.Exited() { - log.Fatal(e) - } - - if got := buf.String(); got != want { - log.Fatalf("opts=%q\n\t got:%q\n\twant:%q", opts, got, want) - } -} - -func main() { - go testsuite.ReceiveSignals() - - cred := syscall.Credential{Uid: 1000, Gid: 100} - if err := os.Mkdir("result", 0755); err != nil { - log.Fatal(err) - } - - done := make(chan struct{}) - go func() { - defer close(done) - - testsuite.MustRun( - nil, nil, - "fs_mark", - "-v", - "-d", "/sdcard/fs_mark", - "-l", "result/fs_mark.log", - ) - }() - - log.Println("checking malformed setuid/setgid representation") - checkBadOpts(&cred, "setuid=ff", "sharefs: invalid value for option setuid\n") - checkBadOpts(&cred, "setgid=ff", "sharefs: invalid value for option setgid\n") - - log.Println("checking bounds check for setuid/setgid") - checkBadOpts(&cred, "setuid=0", "sharefs: invalid value for option setuid\n") - checkBadOpts(&cred, "setgid=0", "sharefs: invalid value for option setgid\n") - checkBadOpts(&cred, "setuid=-1", "sharefs: invalid value for option setuid\n") - checkBadOpts(&cred, "setgid=-1", "sharefs: invalid value for option setgid\n") - - log.Println("checking non-root setuid/setgid") - checkBadOpts(&cred, "setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") - checkBadOpts(&cred, "setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") - checkBadOpts(&cred, "setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") - checkBadOpts(&cred, "mkdir", "sharefs: mkdir has no effect when not starting as root\n") - - log.Println("checking root without setuid/setgid") - checkBadOpts(nil, "allow_other", "sharefs: setuid and setgid must not be 0\n") - checkBadOpts(nil, "setuid=1023", "sharefs: setuid and setgid must not be 0\n") - checkBadOpts(nil, "setgid=1023", "sharefs: setuid and setgid must not be 0\n") - - log.Println("verifying mount point") - if err := os.Remove("/mnt"); err != nil { - log.Fatal(err) - } - - log.Println("checking unprivileged mount/unmount") - testsuite.MustRun(&cred, nil, "mkdir", "/tmp/sdcard", "/tmp/persistent") - testsuite.MustRun(&cred, nil, "sharefs", "-o", "source=/tmp/persistent", "/tmp/sdcard") - testsuite.MustRun(&cred, nil, "touch", "/tmp/sdcard/check") - testsuite.MustRun(&cred, nil, "umount", "/tmp/sdcard") - testsuite.MustRun(&cred, nil, "rm", "/tmp/persistent/check") - testsuite.MustRun(&cred, nil, "rmdir", "/tmp/sdcard", "/tmp/persistent") - - log.Println("waiting for fs_mark to complete") - <-done - - const backingDir = "/var/lib/sdcard" - sharefsCred := syscall.Credential{Uid: 1023, Gid: 1023} - log.Println("checking permissions") - testsuite.MustRun(&sharefsCred, nil, "touch", backingDir+"/fs_mark/.check") - testsuite.MustRun(&sharefsCred, nil, "rm", backingDir+"/fs_mark/.check") - testsuite.MustRun(&cred, nil, "rm", "-rf", "/sdcard/fs_mark") - if _, err := os.ReadDir(backingDir + "/fs_mark"); err == nil { - log.Fatal("fs_mark directory was not removed") - } else if !errors.Is(err, os.ErrNotExist) { - log.Fatal(err) - } -} diff --git a/test/sharefs/raceattr.go b/test/sharefs/raceattr.go deleted file mode 100644 index 412cb2b3..00000000 --- a/test/sharefs/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()) - } -} |
