Compare commits
	
		
			No commits in common. "d8e9d71f87db912d73345fec2f9fcb61b25231cf" and "71135f339a4384ec67fa87ed6624a09168a7e52b" have entirely different histories.
		
	
	
		
			d8e9d71f87
			...
			71135f339a
		
	
		
@ -22,23 +22,6 @@ jobs:
 | 
				
			|||||||
          path: result/*
 | 
					          path: result/*
 | 
				
			||||||
          retention-days: 1
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fpkg:
 | 
					 | 
				
			||||||
    name: Fpkg
 | 
					 | 
				
			||||||
    runs-on: nix
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - name: Checkout
 | 
					 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Run NixOS test
 | 
					 | 
				
			||||||
        run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.fpkg
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Upload test output
 | 
					 | 
				
			||||||
        uses: actions/upload-artifact@v3
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          name: "fpkg-vm-output"
 | 
					 | 
				
			||||||
          path: result/*
 | 
					 | 
				
			||||||
          retention-days: 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  race:
 | 
					  race:
 | 
				
			||||||
    name: Data race detector
 | 
					    name: Data race detector
 | 
				
			||||||
    runs-on: nix
 | 
					    runs-on: nix
 | 
				
			||||||
@ -60,7 +43,6 @@ jobs:
 | 
				
			|||||||
    name: Flake checks
 | 
					    name: Flake checks
 | 
				
			||||||
    needs:
 | 
					    needs:
 | 
				
			||||||
      - fortify
 | 
					      - fortify
 | 
				
			||||||
      - fpkg
 | 
					 | 
				
			||||||
      - race
 | 
					      - race
 | 
				
			||||||
    runs-on: nix
 | 
					    runs-on: nix
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
				
			|||||||
@ -7,9 +7,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
  stdenv,
 | 
					 | 
				
			||||||
  closureInfo,
 | 
					 | 
				
			||||||
  writeScript,
 | 
					  writeScript,
 | 
				
			||||||
 | 
					  writeScriptBin,
 | 
				
			||||||
  runtimeShell,
 | 
					  runtimeShell,
 | 
				
			||||||
  writeText,
 | 
					  writeText,
 | 
				
			||||||
  symlinkJoin,
 | 
					  symlinkJoin,
 | 
				
			||||||
@ -17,15 +16,12 @@
 | 
				
			|||||||
  runCommand,
 | 
					  runCommand,
 | 
				
			||||||
  fetchFromGitHub,
 | 
					  fetchFromGitHub,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  zstd,
 | 
					 | 
				
			||||||
  nix,
 | 
					  nix,
 | 
				
			||||||
  sqlite,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  name ? throw "name is required",
 | 
					  name ? throw "name is required",
 | 
				
			||||||
  version ? throw "version is required",
 | 
					  version ? throw "version is required",
 | 
				
			||||||
  pname ? "${name}-${version}",
 | 
					  pname ? "${name}-${version}",
 | 
				
			||||||
  modules ? [ ],
 | 
					  modules ? [ ],
 | 
				
			||||||
  nixosModules ? [ ],
 | 
					 | 
				
			||||||
  script ? ''
 | 
					  script ? ''
 | 
				
			||||||
    exec "$SHELL" "$@"
 | 
					    exec "$SHELL" "$@"
 | 
				
			||||||
  '',
 | 
					  '',
 | 
				
			||||||
@ -77,8 +73,6 @@ let
 | 
				
			|||||||
        etc.nixpkgs.source = nixpkgs.outPath;
 | 
					        etc.nixpkgs.source = nixpkgs.outPath;
 | 
				
			||||||
        systemPackages = [ pkgs.nix ];
 | 
					        systemPackages = [ pkgs.nix ];
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					 | 
				
			||||||
      imports = nixosModules;
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  nixos = nixpkgs.lib.nixosSystem {
 | 
					  nixos = nixpkgs.lib.nixosSystem {
 | 
				
			||||||
    inherit system;
 | 
					    inherit system;
 | 
				
			||||||
@ -171,7 +165,11 @@ let
 | 
				
			|||||||
          broadcast = { };
 | 
					          broadcast = { };
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0);
 | 
					    enablements =
 | 
				
			||||||
 | 
					      (if allow_wayland then 1 else 0)
 | 
				
			||||||
 | 
					      + (if allow_x11 then 2 else 0)
 | 
				
			||||||
 | 
					      + (if allow_dbus then 4 else 0)
 | 
				
			||||||
 | 
					      + (if allow_pulse then 8 else 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mesa = if gpu then mesaWrappers else null;
 | 
					    mesa = if gpu then mesaWrappers else null;
 | 
				
			||||||
    nix_gl = if gpu then nixGL else null;
 | 
					    nix_gl = if gpu then nixGL else null;
 | 
				
			||||||
@ -180,73 +178,26 @@ let
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stdenv.mkDerivation {
 | 
					writeScriptBin "build-fpkg-${pname}" ''
 | 
				
			||||||
  name = "${pname}.pkg";
 | 
					  #!${runtimeShell} -el
 | 
				
			||||||
  inherit version;
 | 
					  OUT="$(mktemp -d)"
 | 
				
			||||||
  __structuredAttrs = true;
 | 
					  TAR="$(mktemp -u)"
 | 
				
			||||||
 | 
					  set -x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  nativeBuildInputs = [
 | 
					  nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
 | 
				
			||||||
    zstd
 | 
					  nix store --store "$OUT" optimise
 | 
				
			||||||
    nix
 | 
					  chmod -R +r "$OUT/nix/var"
 | 
				
			||||||
    sqlite
 | 
					  nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
 | 
				
			||||||
  ];
 | 
					    "${homeManagerConfiguration.activationPackage}" \
 | 
				
			||||||
 | 
					    "${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
 | 
				
			||||||
 | 
					  mkdir -p "$OUT/etc"
 | 
				
			||||||
 | 
					  tar -C "$OUT/etc" -xf "${etc}/etc.tar"
 | 
				
			||||||
 | 
					  cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  buildCommand = ''
 | 
					  # creating an intermediate file improves zstd performance
 | 
				
			||||||
    NIX_ROOT="$(mktemp -d)"
 | 
					  tar -C "$OUT" -cf "$TAR" .
 | 
				
			||||||
    export USER="nobody"
 | 
					  chmod +w -R "$OUT" && rm -rf "$OUT"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # create bootstrap store
 | 
					  zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
 | 
				
			||||||
    bootstrapClosureInfo="${
 | 
					  rm "$TAR"
 | 
				
			||||||
      closureInfo {
 | 
					''
 | 
				
			||||||
        rootPaths = [
 | 
					 | 
				
			||||||
          nix
 | 
					 | 
				
			||||||
          nixos.config.system.build.toplevel
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }"
 | 
					 | 
				
			||||||
    echo "copying bootstrap store paths..."
 | 
					 | 
				
			||||||
    mkdir -p "$NIX_ROOT/nix/store"
 | 
					 | 
				
			||||||
    xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
 | 
					 | 
				
			||||||
    NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
 | 
					 | 
				
			||||||
    NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
 | 
					 | 
				
			||||||
    sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
 | 
					 | 
				
			||||||
    chmod -R +r "$NIX_ROOT/nix/var"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # create binary cache
 | 
					 | 
				
			||||||
    closureInfo="${
 | 
					 | 
				
			||||||
      closureInfo {
 | 
					 | 
				
			||||||
        rootPaths =
 | 
					 | 
				
			||||||
          [
 | 
					 | 
				
			||||||
            homeManagerConfiguration.activationPackage
 | 
					 | 
				
			||||||
            launcher
 | 
					 | 
				
			||||||
          ]
 | 
					 | 
				
			||||||
          ++ optionals gpu [
 | 
					 | 
				
			||||||
            mesaWrappers
 | 
					 | 
				
			||||||
            nixGL
 | 
					 | 
				
			||||||
          ];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }"
 | 
					 | 
				
			||||||
    echo "copying application paths..."
 | 
					 | 
				
			||||||
    TMP_STORE="$(mktemp -d)"
 | 
					 | 
				
			||||||
    mkdir -p "$TMP_STORE/nix/store"
 | 
					 | 
				
			||||||
    xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
 | 
					 | 
				
			||||||
    NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
 | 
					 | 
				
			||||||
    sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
 | 
					 | 
				
			||||||
    NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
 | 
					 | 
				
			||||||
        --verbose --log-format raw-with-logs \
 | 
					 | 
				
			||||||
        copy --all --no-check-sigs --to \
 | 
					 | 
				
			||||||
        "file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # package /etc
 | 
					 | 
				
			||||||
    mkdir -p "$NIX_ROOT/etc"
 | 
					 | 
				
			||||||
    tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # write metadata
 | 
					 | 
				
			||||||
    cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # create an intermediate file to improve zstd performance
 | 
					 | 
				
			||||||
    INTER="$(mktemp)"
 | 
					 | 
				
			||||||
    tar -C "$NIX_ROOT" -cf "$INTER" .
 | 
					 | 
				
			||||||
    zstd -T0 -19 -fo "$out" "$INTER"
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										191
									
								
								cmd/fpkg/install.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								cmd/fpkg/install.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"flag"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func actionInstall(args []string) {
 | 
				
			||||||
 | 
						set := flag.NewFlagSet("install", flag.ExitOnError)
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							dropShellInstall  bool
 | 
				
			||||||
 | 
							dropShellActivate bool
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						set.BoolVar(&dropShellInstall, "si", false, "Drop to a shell on installation")
 | 
				
			||||||
 | 
						set.BoolVar(&dropShellActivate, "sa", false, "Drop to a shell on activation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore errors; set is set for ExitOnError.
 | 
				
			||||||
 | 
						_ = set.Parse(args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args = set.Args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(args) != 1 {
 | 
				
			||||||
 | 
							log.Fatal("invalid argument")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pkgPath := args[0]
 | 
				
			||||||
 | 
						if !path.IsAbs(pkgPath) {
 | 
				
			||||||
 | 
							if dir, err := os.Getwd(); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot get current directory: %v", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								pkgPath = path.Join(dir, pkgPath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Look up paths to programs started by fpkg.
 | 
				
			||||||
 | 
							This is done here to ease error handling as cleanup is not yet required.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							_     = lookPath("zstd")
 | 
				
			||||||
 | 
							tar   = lookPath("tar")
 | 
				
			||||||
 | 
							chmod = lookPath("chmod")
 | 
				
			||||||
 | 
							rm    = lookPath("rm")
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Extract package and set up for cleanup.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var workDir string
 | 
				
			||||||
 | 
						if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot create temporary directory: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							workDir = p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						cleanup := func() {
 | 
				
			||||||
 | 
							// should be faster than a native implementation
 | 
				
			||||||
 | 
							mustRun(chmod, "-R", "+w", workDir)
 | 
				
			||||||
 | 
							mustRun(rm, "-rf", workDir)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						beforeRunFail.Store(&cleanup)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mustRun(tar, "-C", workDir, "-xf", pkgPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Parse bundle and app metadata, do pre-install checks.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
				
			||||||
 | 
						pathSet := pathSetByApp(bundle.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app := bundle
 | 
				
			||||||
 | 
						if s, err := os.Stat(pathSet.metaPath); err != nil {
 | 
				
			||||||
 | 
							if !os.IsNotExist(err) {
 | 
				
			||||||
 | 
								cleanup()
 | 
				
			||||||
 | 
								log.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// did not modify app, clean installation condition met later
 | 
				
			||||||
 | 
						} else if s.IsDir() {
 | 
				
			||||||
 | 
							cleanup()
 | 
				
			||||||
 | 
							log.Fatalf("metadata path %q is not a file", pathSet.metaPath)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							app = loadBundleInfo(pathSet.metaPath, cleanup)
 | 
				
			||||||
 | 
							if app.ID != bundle.ID {
 | 
				
			||||||
 | 
								cleanup()
 | 
				
			||||||
 | 
								log.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// sec: should verify credentials
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if app != bundle {
 | 
				
			||||||
 | 
							// do not try to re-install
 | 
				
			||||||
 | 
							if app.NixGL == bundle.NixGL &&
 | 
				
			||||||
 | 
								app.CurrentSystem == bundle.CurrentSystem &&
 | 
				
			||||||
 | 
								app.Launcher == bundle.Launcher &&
 | 
				
			||||||
 | 
								app.ActivationPackage == bundle.ActivationPackage {
 | 
				
			||||||
 | 
								cleanup()
 | 
				
			||||||
 | 
								log.Printf("package %q is identical to local application %q", pkgPath, app.ID)
 | 
				
			||||||
 | 
								internal.Exit(0)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// AppID determines uid
 | 
				
			||||||
 | 
							if app.AppID != bundle.AppID {
 | 
				
			||||||
 | 
								cleanup()
 | 
				
			||||||
 | 
								log.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// sec: should compare version string
 | 
				
			||||||
 | 
							fmsg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fmsg.Verbosef("application %q clean installation", bundle.ID)
 | 
				
			||||||
 | 
							// sec: should install credentials
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Setup steps for files owned by the target user.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						withCacheDir("install", []string{
 | 
				
			||||||
 | 
							// export inner bundle path in the environment
 | 
				
			||||||
 | 
							"export BUNDLE=" + fst.Tmp + "/bundle",
 | 
				
			||||||
 | 
							// replace inner /etc
 | 
				
			||||||
 | 
							"mkdir -p etc",
 | 
				
			||||||
 | 
							"chmod -R +w etc",
 | 
				
			||||||
 | 
							"rm -rf etc",
 | 
				
			||||||
 | 
							"cp -dRf $BUNDLE/etc etc",
 | 
				
			||||||
 | 
							// replace inner /nix
 | 
				
			||||||
 | 
							"mkdir -p nix",
 | 
				
			||||||
 | 
							"chmod -R +w nix",
 | 
				
			||||||
 | 
							"rm -rf nix",
 | 
				
			||||||
 | 
							"cp -dRf /nix nix",
 | 
				
			||||||
 | 
							// copy from binary cache
 | 
				
			||||||
 | 
							"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
 | 
				
			||||||
 | 
							// deduplicate nix store
 | 
				
			||||||
 | 
							"nix store --offline --store $PWD optimise",
 | 
				
			||||||
 | 
							// make cache directory world-readable for autoetc
 | 
				
			||||||
 | 
							"chmod 0755 .",
 | 
				
			||||||
 | 
						}, workDir, bundle, pathSet, dropShellInstall, cleanup)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if bundle.GPU {
 | 
				
			||||||
 | 
							withCacheDir("mesa-wrappers", []string{
 | 
				
			||||||
 | 
								// link nixGL mesa wrappers
 | 
				
			||||||
 | 
								"mkdir -p nix/.nixGL",
 | 
				
			||||||
 | 
								"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
 | 
				
			||||||
 | 
								"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
 | 
				
			||||||
 | 
							}, workDir, bundle, pathSet, false, cleanup)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Activate home-manager generation.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						withNixDaemon("activate", []string{
 | 
				
			||||||
 | 
							// clean up broken links
 | 
				
			||||||
 | 
							"mkdir -p .local/state/{nix,home-manager}",
 | 
				
			||||||
 | 
							"chmod -R +w .local/state/{nix,home-manager}",
 | 
				
			||||||
 | 
							"rm -rf .local/state/{nix,home-manager}",
 | 
				
			||||||
 | 
							// run activation script
 | 
				
			||||||
 | 
							bundle.ActivationPackage + "/activate",
 | 
				
			||||||
 | 
						}, false, func(config *fst.Config) *fst.Config { return config }, bundle, pathSet, dropShellActivate, cleanup)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Installation complete. Write metadata to block re-installs or downgrades.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// serialise metadata to ensure consistency
 | 
				
			||||||
 | 
						if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
 | 
				
			||||||
 | 
							cleanup()
 | 
				
			||||||
 | 
							log.Fatalf("cannot create metadata file: %v", err)
 | 
				
			||||||
 | 
						} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
 | 
				
			||||||
 | 
							cleanup()
 | 
				
			||||||
 | 
							log.Fatalf("cannot write metadata: %v", err)
 | 
				
			||||||
 | 
						} else if err = f.Close(); err != nil {
 | 
				
			||||||
 | 
							log.Printf("cannot close metadata file: %v", err)
 | 
				
			||||||
 | 
							// not fatal
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
 | 
				
			||||||
 | 
							cleanup()
 | 
				
			||||||
 | 
							log.Fatalf("cannot rename metadata file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cleanup()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										410
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							
							
						
						
									
										410
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							@ -1,402 +1,50 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"flag"
 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	init0 "git.gensokyo.uk/security/fortify/internal/app/init"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const shellPath = "/run/current-system/sw/bin/bash"
 | 
					const shellPath = "/run/current-system/sw/bin/bash"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	errSuccess = errors.New("success")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	std sys.State = new(sys.Std)
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	fmsg.Prepare("fpkg")
 | 
					 | 
				
			||||||
	if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
						if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot set $SHELL: %v", err)
 | 
							log.Fatalf("cannot set $SHELL: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					var (
 | 
				
			||||||
	// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
 | 
					 | 
				
			||||||
	init0.TryArgv0()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
					 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
					 | 
				
			||||||
		// not fatal: this program runs as the privileged user
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if os.Geteuid() == 0 {
 | 
					 | 
				
			||||||
		log.Fatal("this program must not run as root")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
					 | 
				
			||||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
					 | 
				
			||||||
	defer stop() // unreachable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
	flagVerbose bool
 | 
						flagVerbose bool
 | 
				
			||||||
		flagDropShell bool
 | 
					)
 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
 | 
					 | 
				
			||||||
		fmsg.Store(flagVerbose)
 | 
					 | 
				
			||||||
		if flagVerbose {
 | 
					 | 
				
			||||||
			seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}).
 | 
					 | 
				
			||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
					 | 
				
			||||||
		Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// internal commands
 | 
					func init() {
 | 
				
			||||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
						flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
 | 
				
			||||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{
 | 
					func main() {
 | 
				
			||||||
		var (
 | 
						fmsg.Prepare("fpkg")
 | 
				
			||||||
			flagDropShellActivate bool
 | 
					
 | 
				
			||||||
		)
 | 
						flag.Parse()
 | 
				
			||||||
		c.NewCommand("install", "Install an application from its package", func(args []string) error {
 | 
						fmsg.Store(flagVerbose)
 | 
				
			||||||
			if len(args) != 1 {
 | 
					
 | 
				
			||||||
				log.Println("invalid argument")
 | 
						args := flag.Args()
 | 
				
			||||||
				return syscall.EINVAL
 | 
						if len(args) < 1 {
 | 
				
			||||||
			}
 | 
							log.Fatal("invalid argument")
 | 
				
			||||||
			pkgPath := args[0]
 | 
						}
 | 
				
			||||||
			if !path.IsAbs(pkgPath) {
 | 
					
 | 
				
			||||||
				if dir, err := os.Getwd(); err != nil {
 | 
						switch args[0] {
 | 
				
			||||||
					log.Printf("cannot get current directory: %v", err)
 | 
						case "install":
 | 
				
			||||||
					return err
 | 
							actionInstall(args[1:])
 | 
				
			||||||
				} else {
 | 
						case "start":
 | 
				
			||||||
					pkgPath = path.Join(dir, pkgPath)
 | 
							actionStart(args[1:])
 | 
				
			||||||
				}
 | 
					
 | 
				
			||||||
			}
 | 
						default:
 | 
				
			||||||
 | 
							log.Fatal("invalid argument")
 | 
				
			||||||
			/*
 | 
						}
 | 
				
			||||||
				Look up paths to programs started by fpkg.
 | 
					
 | 
				
			||||||
				This is done here to ease error handling as cleanup is not yet required.
 | 
						internal.Exit(0)
 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var (
 | 
					 | 
				
			||||||
				_     = lookPath("zstd")
 | 
					 | 
				
			||||||
				tar   = lookPath("tar")
 | 
					 | 
				
			||||||
				chmod = lookPath("chmod")
 | 
					 | 
				
			||||||
				rm    = lookPath("rm")
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Extract package and set up for cleanup.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var workDir string
 | 
					 | 
				
			||||||
			if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
 | 
					 | 
				
			||||||
				log.Printf("cannot create temporary directory: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				workDir = p
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			cleanup := func() {
 | 
					 | 
				
			||||||
				// should be faster than a native implementation
 | 
					 | 
				
			||||||
				mustRun(chmod, "-R", "+w", workDir)
 | 
					 | 
				
			||||||
				mustRun(rm, "-rf", workDir)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			beforeRunFail.Store(&cleanup)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			mustRun(tar, "-C", workDir, "-xf", pkgPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Parse bundle and app metadata, do pre-install checks.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
					 | 
				
			||||||
			pathSet := pathSetByApp(bundle.ID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			app := bundle
 | 
					 | 
				
			||||||
			if s, err := os.Stat(pathSet.metaPath); err != nil {
 | 
					 | 
				
			||||||
				if !os.IsNotExist(err) {
 | 
					 | 
				
			||||||
					cleanup()
 | 
					 | 
				
			||||||
					log.Printf("cannot access %q: %v", pathSet.metaPath, err)
 | 
					 | 
				
			||||||
					return err
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				// did not modify app, clean installation condition met later
 | 
					 | 
				
			||||||
			} else if s.IsDir() {
 | 
					 | 
				
			||||||
				cleanup()
 | 
					 | 
				
			||||||
				log.Printf("metadata path %q is not a file", pathSet.metaPath)
 | 
					 | 
				
			||||||
				return syscall.EBADMSG
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				app = loadBundleInfo(pathSet.metaPath, cleanup)
 | 
					 | 
				
			||||||
				if app.ID != bundle.ID {
 | 
					 | 
				
			||||||
					cleanup()
 | 
					 | 
				
			||||||
					log.Printf("app %q claims to have identifier %q",
 | 
					 | 
				
			||||||
						bundle.ID, app.ID)
 | 
					 | 
				
			||||||
					return syscall.EBADE
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				// sec: should verify credentials
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if app != bundle {
 | 
					 | 
				
			||||||
				// do not try to re-install
 | 
					 | 
				
			||||||
				if app.NixGL == bundle.NixGL &&
 | 
					 | 
				
			||||||
					app.CurrentSystem == bundle.CurrentSystem &&
 | 
					 | 
				
			||||||
					app.Launcher == bundle.Launcher &&
 | 
					 | 
				
			||||||
					app.ActivationPackage == bundle.ActivationPackage {
 | 
					 | 
				
			||||||
					cleanup()
 | 
					 | 
				
			||||||
					log.Printf("package %q is identical to local application %q",
 | 
					 | 
				
			||||||
						pkgPath, app.ID)
 | 
					 | 
				
			||||||
					return errSuccess
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// AppID determines uid
 | 
					 | 
				
			||||||
				if app.AppID != bundle.AppID {
 | 
					 | 
				
			||||||
					cleanup()
 | 
					 | 
				
			||||||
					log.Printf("package %q app id %d differs from installed %d",
 | 
					 | 
				
			||||||
						pkgPath, bundle.AppID, app.AppID)
 | 
					 | 
				
			||||||
					return syscall.EBADE
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// sec: should compare version string
 | 
					 | 
				
			||||||
				fmsg.Verbosef("installing application %q version %q over local %q",
 | 
					 | 
				
			||||||
					bundle.ID, bundle.Version, app.Version)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				fmsg.Verbosef("application %q clean installation", bundle.ID)
 | 
					 | 
				
			||||||
				// sec: should install credentials
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Setup steps for files owned by the target user.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			withCacheDir(ctx, "install", []string{
 | 
					 | 
				
			||||||
				// export inner bundle path in the environment
 | 
					 | 
				
			||||||
				"export BUNDLE=" + fst.Tmp + "/bundle",
 | 
					 | 
				
			||||||
				// replace inner /etc
 | 
					 | 
				
			||||||
				"mkdir -p etc",
 | 
					 | 
				
			||||||
				"chmod -R +w etc",
 | 
					 | 
				
			||||||
				"rm -rf etc",
 | 
					 | 
				
			||||||
				"cp -dRf $BUNDLE/etc etc",
 | 
					 | 
				
			||||||
				// replace inner /nix
 | 
					 | 
				
			||||||
				"mkdir -p nix",
 | 
					 | 
				
			||||||
				"chmod -R +w nix",
 | 
					 | 
				
			||||||
				"rm -rf nix",
 | 
					 | 
				
			||||||
				"cp -dRf /nix nix",
 | 
					 | 
				
			||||||
				// copy from binary cache
 | 
					 | 
				
			||||||
				"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
 | 
					 | 
				
			||||||
				// deduplicate nix store
 | 
					 | 
				
			||||||
				"nix store --offline --store $PWD optimise",
 | 
					 | 
				
			||||||
				// make cache directory world-readable for autoetc
 | 
					 | 
				
			||||||
				"chmod 0755 .",
 | 
					 | 
				
			||||||
			}, workDir, bundle, pathSet, flagDropShell, cleanup)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if bundle.GPU {
 | 
					 | 
				
			||||||
				withCacheDir(ctx, "mesa-wrappers", []string{
 | 
					 | 
				
			||||||
					// link nixGL mesa wrappers
 | 
					 | 
				
			||||||
					"mkdir -p nix/.nixGL",
 | 
					 | 
				
			||||||
					"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
 | 
					 | 
				
			||||||
					"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
 | 
					 | 
				
			||||||
				}, workDir, bundle, pathSet, false, cleanup)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Activate home-manager generation.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			withNixDaemon(ctx, "activate", []string{
 | 
					 | 
				
			||||||
				// clean up broken links
 | 
					 | 
				
			||||||
				"mkdir -p .local/state/{nix,home-manager}",
 | 
					 | 
				
			||||||
				"chmod -R +w .local/state/{nix,home-manager}",
 | 
					 | 
				
			||||||
				"rm -rf .local/state/{nix,home-manager}",
 | 
					 | 
				
			||||||
				// run activation script
 | 
					 | 
				
			||||||
				bundle.ActivationPackage + "/activate",
 | 
					 | 
				
			||||||
			}, false, func(config *fst.Config) *fst.Config { return config },
 | 
					 | 
				
			||||||
				bundle, pathSet, flagDropShellActivate, cleanup)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Installation complete. Write metadata to block re-installs or downgrades.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// serialise metadata to ensure consistency
 | 
					 | 
				
			||||||
			if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
 | 
					 | 
				
			||||||
				cleanup()
 | 
					 | 
				
			||||||
				log.Printf("cannot create metadata file: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
 | 
					 | 
				
			||||||
				cleanup()
 | 
					 | 
				
			||||||
				log.Printf("cannot write metadata: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else if err = f.Close(); err != nil {
 | 
					 | 
				
			||||||
				log.Printf("cannot close metadata file: %v", err)
 | 
					 | 
				
			||||||
				// not fatal
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
 | 
					 | 
				
			||||||
				cleanup()
 | 
					 | 
				
			||||||
				log.Printf("cannot rename metadata file: %v", err)
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			cleanup()
 | 
					 | 
				
			||||||
			return errSuccess
 | 
					 | 
				
			||||||
		}).
 | 
					 | 
				
			||||||
			Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		var (
 | 
					 | 
				
			||||||
			flagDropShellNixGL bool
 | 
					 | 
				
			||||||
			flagAutoDrivers    bool
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		c.NewCommand("start", "Start an application", func(args []string) error {
 | 
					 | 
				
			||||||
			if len(args) < 1 {
 | 
					 | 
				
			||||||
				log.Println("invalid argument")
 | 
					 | 
				
			||||||
				return syscall.EINVAL
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Parse app metadata.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			id := args[0]
 | 
					 | 
				
			||||||
			pathSet := pathSetByApp(id)
 | 
					 | 
				
			||||||
			app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
					 | 
				
			||||||
			if app.ID != id {
 | 
					 | 
				
			||||||
				log.Printf("app %q claims to have identifier %q", id, app.ID)
 | 
					 | 
				
			||||||
				return syscall.EBADE
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Prepare nixGL.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if app.GPU && flagAutoDrivers {
 | 
					 | 
				
			||||||
				withNixDaemon(ctx, "nix-gl", []string{
 | 
					 | 
				
			||||||
					"mkdir -p /nix/.nixGL/auto",
 | 
					 | 
				
			||||||
					"rm -rf /nix/.nixGL/auto",
 | 
					 | 
				
			||||||
					"export NIXPKGS_ALLOW_UNFREE=1",
 | 
					 | 
				
			||||||
					"nix build --impure " +
 | 
					 | 
				
			||||||
						"--out-link /nix/.nixGL/auto/opengl " +
 | 
					 | 
				
			||||||
						"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
					 | 
				
			||||||
						"path:" + app.NixGL,
 | 
					 | 
				
			||||||
					"nix build --impure " +
 | 
					 | 
				
			||||||
						"--out-link /nix/.nixGL/auto/vulkan " +
 | 
					 | 
				
			||||||
						"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
					 | 
				
			||||||
						"path:" + app.NixGL + "#nixVulkanNvidia",
 | 
					 | 
				
			||||||
				}, true, func(config *fst.Config) *fst.Config {
 | 
					 | 
				
			||||||
					config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
					 | 
				
			||||||
						{Src: "/etc/resolv.conf"},
 | 
					 | 
				
			||||||
						{Src: "/sys/block"},
 | 
					 | 
				
			||||||
						{Src: "/sys/bus"},
 | 
					 | 
				
			||||||
						{Src: "/sys/class"},
 | 
					 | 
				
			||||||
						{Src: "/sys/dev"},
 | 
					 | 
				
			||||||
						{Src: "/sys/devices"},
 | 
					 | 
				
			||||||
					}...)
 | 
					 | 
				
			||||||
					appendGPUFilesystem(config)
 | 
					 | 
				
			||||||
					return config
 | 
					 | 
				
			||||||
				}, app, pathSet, flagDropShellNixGL, func() {})
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Create app configuration.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			argv := make([]string, 1, len(args))
 | 
					 | 
				
			||||||
			if !flagDropShell {
 | 
					 | 
				
			||||||
				argv[0] = app.Launcher
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				argv[0] = shellPath
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			argv = append(argv, args[1:]...)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			config := &fst.Config{
 | 
					 | 
				
			||||||
				ID:      app.ID,
 | 
					 | 
				
			||||||
				Command: argv,
 | 
					 | 
				
			||||||
				Confinement: fst.ConfinementConfig{
 | 
					 | 
				
			||||||
					AppID:    app.AppID,
 | 
					 | 
				
			||||||
					Groups:   app.Groups,
 | 
					 | 
				
			||||||
					Username: "fortify",
 | 
					 | 
				
			||||||
					Inner:    path.Join("/data/data", app.ID),
 | 
					 | 
				
			||||||
					Outer:    pathSet.homeDir,
 | 
					 | 
				
			||||||
					Sandbox: &fst.SandboxConfig{
 | 
					 | 
				
			||||||
						Hostname:      formatHostname(app.Name),
 | 
					 | 
				
			||||||
						UserNS:        app.UserNS,
 | 
					 | 
				
			||||||
						Net:           app.Net,
 | 
					 | 
				
			||||||
						Dev:           app.Dev,
 | 
					 | 
				
			||||||
						Syscall:       &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
 | 
					 | 
				
			||||||
						NoNewSession:  app.NoNewSession || flagDropShell,
 | 
					 | 
				
			||||||
						MapRealUID:    app.MapRealUID,
 | 
					 | 
				
			||||||
						DirectWayland: app.DirectWayland,
 | 
					 | 
				
			||||||
						Filesystem: []*fst.FilesystemConfig{
 | 
					 | 
				
			||||||
							{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
					 | 
				
			||||||
							{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
					 | 
				
			||||||
							{Src: "/etc/resolv.conf"},
 | 
					 | 
				
			||||||
							{Src: "/sys/block"},
 | 
					 | 
				
			||||||
							{Src: "/sys/bus"},
 | 
					 | 
				
			||||||
							{Src: "/sys/class"},
 | 
					 | 
				
			||||||
							{Src: "/sys/dev"},
 | 
					 | 
				
			||||||
							{Src: "/sys/devices"},
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						Link: [][2]string{
 | 
					 | 
				
			||||||
							{app.CurrentSystem, "/run/current-system"},
 | 
					 | 
				
			||||||
							{"/run/current-system/sw/bin", "/bin"},
 | 
					 | 
				
			||||||
							{"/run/current-system/sw/bin", "/usr/bin"},
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
					 | 
				
			||||||
						AutoEtc: true,
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					ExtraPerms: []*fst.ExtraPermConfig{
 | 
					 | 
				
			||||||
						{Path: dataHome, Execute: true},
 | 
					 | 
				
			||||||
						{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					SystemBus:   app.SystemBus,
 | 
					 | 
				
			||||||
					SessionBus:  app.SessionBus,
 | 
					 | 
				
			||||||
					Enablements: app.Enablements,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Expose GPU devices.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if app.GPU {
 | 
					 | 
				
			||||||
				config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
 | 
					 | 
				
			||||||
					&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
 | 
					 | 
				
			||||||
				appendGPUFilesystem(config)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			/*
 | 
					 | 
				
			||||||
				Spawn app.
 | 
					 | 
				
			||||||
			*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			mustRunApp(ctx, config, func() {})
 | 
					 | 
				
			||||||
			return errSuccess
 | 
					 | 
				
			||||||
		}).
 | 
					 | 
				
			||||||
			Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
 | 
					 | 
				
			||||||
			Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.MustParse(os.Args[1:], func(err error) {
 | 
					 | 
				
			||||||
		fmsg.Verbosef("command returned %v", err)
 | 
					 | 
				
			||||||
		if errors.Is(err, errSuccess) {
 | 
					 | 
				
			||||||
			fmsg.BeforeExit()
 | 
					 | 
				
			||||||
			os.Exit(0)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	log.Fatal("unreachable")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -70,32 +69,3 @@ func pathSetByApp(id string) *appPathSet {
 | 
				
			|||||||
	pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
						pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
				
			||||||
	return pathSet
 | 
						return pathSet
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func appendGPUFilesystem(config *fst.Config) {
 | 
					 | 
				
			||||||
	config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
					 | 
				
			||||||
		// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
 | 
					 | 
				
			||||||
		{Src: "/dev/dri", Device: true},
 | 
					 | 
				
			||||||
		// mali
 | 
					 | 
				
			||||||
		{Src: "/dev/mali", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/mali0", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/umplock", Device: true},
 | 
					 | 
				
			||||||
		// nvidia
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidiactl", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia-modeset", Device: true},
 | 
					 | 
				
			||||||
		// nvidia OpenCL/CUDA
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia-uvm", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia-uvm-tools", Device: true},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
 | 
					 | 
				
			||||||
		{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
 | 
					 | 
				
			||||||
	}...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,28 +1,65 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
					const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
				
			||||||
	rs := new(fst.RunState)
 | 
					 | 
				
			||||||
	a := app.MustNew(std)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if sa, err := a.Seal(config); err != nil {
 | 
					var (
 | 
				
			||||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
						Fmain = compPoison
 | 
				
			||||||
		rs.ExitCode = 1
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fortifyApp(config *fst.Config, beforeFail func()) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							cmd *exec.Cmd
 | 
				
			||||||
 | 
							st  io.WriteCloser
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if p, ok := internal.Path(Fmain); !ok {
 | 
				
			||||||
 | 
							beforeFail()
 | 
				
			||||||
 | 
							log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
 | 
				
			||||||
 | 
						} else if r, w, err := os.Pipe(); err != nil {
 | 
				
			||||||
 | 
							beforeFail()
 | 
				
			||||||
 | 
							log.Fatalf("cannot pipe: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// this updates ExitCode
 | 
							if fmsg.Load() {
 | 
				
			||||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
								cmd = exec.Command(p, "-v", "app", "3")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								cmd = exec.Command(p, "app", "3")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
							cmd.ExtraFiles = []*os.File{r}
 | 
				
			||||||
 | 
							st = w
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if rs.ExitCode != 0 {
 | 
						go func() {
 | 
				
			||||||
 | 
							if err := json.NewEncoder(st).Encode(config); err != nil {
 | 
				
			||||||
			beforeFail()
 | 
								beforeFail()
 | 
				
			||||||
		os.Exit(rs.ExitCode)
 | 
								log.Fatalf("cannot send configuration: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							beforeFail()
 | 
				
			||||||
 | 
							log.Fatalf("cannot start fortify: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := cmd.Wait(); err != nil {
 | 
				
			||||||
 | 
							var exitError *exec.ExitError
 | 
				
			||||||
 | 
							if errors.As(err, &exitError) {
 | 
				
			||||||
 | 
								beforeFail()
 | 
				
			||||||
 | 
								internal.Exit(exitError.ExitCode())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								beforeFail()
 | 
				
			||||||
 | 
								log.Fatalf("cannot wait: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										178
									
								
								cmd/fpkg/start.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								cmd/fpkg/start.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"flag"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func actionStart(args []string) {
 | 
				
			||||||
 | 
						set := flag.NewFlagSet("start", flag.ExitOnError)
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							dropShell      bool
 | 
				
			||||||
 | 
							dropShellNixGL bool
 | 
				
			||||||
 | 
							autoDrivers    bool
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						set.BoolVar(&dropShell, "s", false, "Drop to a shell")
 | 
				
			||||||
 | 
						set.BoolVar(&dropShellNixGL, "sg", false, "Drop to a shell on nixGL build")
 | 
				
			||||||
 | 
						set.BoolVar(&autoDrivers, "autodrivers", false, "Attempt automatic opengl driver detection")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ignore errors; set is set for ExitOnError.
 | 
				
			||||||
 | 
						_ = set.Parse(args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args = set.Args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(args) < 1 {
 | 
				
			||||||
 | 
							log.Fatal("invalid argument")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Parse app metadata.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						id := args[0]
 | 
				
			||||||
 | 
						pathSet := pathSetByApp(id)
 | 
				
			||||||
 | 
						app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
				
			||||||
 | 
						if app.ID != id {
 | 
				
			||||||
 | 
							log.Fatalf("app %q claims to have identifier %q", id, app.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Prepare nixGL.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if app.GPU && autoDrivers {
 | 
				
			||||||
 | 
							withNixDaemon("nix-gl", []string{
 | 
				
			||||||
 | 
								"mkdir -p /nix/.nixGL/auto",
 | 
				
			||||||
 | 
								"rm -rf /nix/.nixGL/auto",
 | 
				
			||||||
 | 
								"export NIXPKGS_ALLOW_UNFREE=1",
 | 
				
			||||||
 | 
								"nix build --impure " +
 | 
				
			||||||
 | 
									"--out-link /nix/.nixGL/auto/opengl " +
 | 
				
			||||||
 | 
									"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
				
			||||||
 | 
									"path:" + app.NixGL,
 | 
				
			||||||
 | 
								"nix build --impure " +
 | 
				
			||||||
 | 
									"--out-link /nix/.nixGL/auto/vulkan " +
 | 
				
			||||||
 | 
									"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
				
			||||||
 | 
									"path:" + app.NixGL + "#nixVulkanNvidia",
 | 
				
			||||||
 | 
							}, true, func(config *fst.Config) *fst.Config {
 | 
				
			||||||
 | 
								config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
									{Src: "/etc/resolv.conf"},
 | 
				
			||||||
 | 
									{Src: "/sys/block"},
 | 
				
			||||||
 | 
									{Src: "/sys/bus"},
 | 
				
			||||||
 | 
									{Src: "/sys/class"},
 | 
				
			||||||
 | 
									{Src: "/sys/dev"},
 | 
				
			||||||
 | 
									{Src: "/sys/devices"},
 | 
				
			||||||
 | 
								}...)
 | 
				
			||||||
 | 
								appendGPUFilesystem(config)
 | 
				
			||||||
 | 
								return config
 | 
				
			||||||
 | 
							}, app, pathSet, dropShellNixGL, func() {})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Create app configuration.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						command := make([]string, 1, len(args))
 | 
				
			||||||
 | 
						if !dropShell {
 | 
				
			||||||
 | 
							command[0] = app.Launcher
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							command[0] = shellPath
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						command = append(command, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						config := &fst.Config{
 | 
				
			||||||
 | 
							ID:      app.ID,
 | 
				
			||||||
 | 
							Command: command,
 | 
				
			||||||
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
 | 
								AppID:    app.AppID,
 | 
				
			||||||
 | 
								Groups:   app.Groups,
 | 
				
			||||||
 | 
								Username: "fortify",
 | 
				
			||||||
 | 
								Inner:    path.Join("/data/data", app.ID),
 | 
				
			||||||
 | 
								Outer:    pathSet.homeDir,
 | 
				
			||||||
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
 | 
									Hostname:      formatHostname(app.Name),
 | 
				
			||||||
 | 
									UserNS:        app.UserNS,
 | 
				
			||||||
 | 
									Net:           app.Net,
 | 
				
			||||||
 | 
									Dev:           app.Dev,
 | 
				
			||||||
 | 
									Syscall:       &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
 | 
				
			||||||
 | 
									NoNewSession:  app.NoNewSession || dropShell,
 | 
				
			||||||
 | 
									MapRealUID:    app.MapRealUID,
 | 
				
			||||||
 | 
									DirectWayland: app.DirectWayland,
 | 
				
			||||||
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
										{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
				
			||||||
 | 
										{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
				
			||||||
 | 
										{Src: "/etc/resolv.conf"},
 | 
				
			||||||
 | 
										{Src: "/sys/block"},
 | 
				
			||||||
 | 
										{Src: "/sys/bus"},
 | 
				
			||||||
 | 
										{Src: "/sys/class"},
 | 
				
			||||||
 | 
										{Src: "/sys/dev"},
 | 
				
			||||||
 | 
										{Src: "/sys/devices"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Link: [][2]string{
 | 
				
			||||||
 | 
										{app.CurrentSystem, "/run/current-system"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/bin"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/usr/bin"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
				
			||||||
 | 
									AutoEtc: true,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								ExtraPerms: []*fst.ExtraPermConfig{
 | 
				
			||||||
 | 
									{Path: dataHome, Execute: true},
 | 
				
			||||||
 | 
									{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								SystemBus:   app.SystemBus,
 | 
				
			||||||
 | 
								SessionBus:  app.SessionBus,
 | 
				
			||||||
 | 
								Enablements: app.Enablements,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Expose GPU devices.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if app.GPU {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
 | 
				
			||||||
 | 
								&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
 | 
				
			||||||
 | 
							appendGPUFilesystem(config)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							Spawn app.
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fortifyApp(config, func() {})
 | 
				
			||||||
 | 
						internal.Exit(0)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func appendGPUFilesystem(config *fst.Config) {
 | 
				
			||||||
 | 
						config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
							// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
 | 
				
			||||||
 | 
							{Src: "/dev/dri", Device: true},
 | 
				
			||||||
 | 
							// mali
 | 
				
			||||||
 | 
							{Src: "/dev/mali", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/mali0", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/umplock", Device: true},
 | 
				
			||||||
 | 
							// nvidia
 | 
				
			||||||
 | 
							{Src: "/dev/nvidiactl", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-modeset", Device: true},
 | 
				
			||||||
 | 
							// nvidia OpenCL/CUDA
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-uvm", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-uvm-tools", Device: true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
 | 
				
			||||||
 | 
						}...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,60 +0,0 @@
 | 
				
			|||||||
{ pkgs, ... }:
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  users.users = {
 | 
					 | 
				
			||||||
    alice = {
 | 
					 | 
				
			||||||
      isNormalUser = true;
 | 
					 | 
				
			||||||
      description = "Alice Foobar";
 | 
					 | 
				
			||||||
      password = "foobar";
 | 
					 | 
				
			||||||
      uid = 1000;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  home-manager.users.alice.home.stateVersion = "24.11";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Automatically login on tty1 as a normal user:
 | 
					 | 
				
			||||||
  services.getty.autologinUser = "alice";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  environment = {
 | 
					 | 
				
			||||||
    variables = {
 | 
					 | 
				
			||||||
      SWAYSOCK = "/tmp/sway-ipc.sock";
 | 
					 | 
				
			||||||
      WLR_RENDERER = "pixman";
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # 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=sway sway && touch /tmp/sway-exit-ok
 | 
					 | 
				
			||||||
    fi
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  programs.sway.enable = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  virtualisation = {
 | 
					 | 
				
			||||||
    diskSize = 6 * 1024;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 zstd performance:
 | 
					 | 
				
			||||||
      "-smp 8"
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  environment.fortify = {
 | 
					 | 
				
			||||||
    enable = true;
 | 
					 | 
				
			||||||
    stateDir = "/var/lib/fortify";
 | 
					 | 
				
			||||||
    users.alice = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,34 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  nixosTest,
 | 
					 | 
				
			||||||
  callPackage,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  system,
 | 
					 | 
				
			||||||
  self,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
let
 | 
					 | 
				
			||||||
  buildPackage = self.buildPackage.${system};
 | 
					 | 
				
			||||||
in
 | 
					 | 
				
			||||||
nixosTest {
 | 
					 | 
				
			||||||
  name = "fpkg";
 | 
					 | 
				
			||||||
  nodes.machine = {
 | 
					 | 
				
			||||||
    environment.etc = {
 | 
					 | 
				
			||||||
      "foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    imports = [
 | 
					 | 
				
			||||||
      ./configuration.nix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      self.nixosModules.fortify
 | 
					 | 
				
			||||||
      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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,48 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  lib,
 | 
					 | 
				
			||||||
  buildPackage,
 | 
					 | 
				
			||||||
  foot,
 | 
					 | 
				
			||||||
  wayland-utils,
 | 
					 | 
				
			||||||
  inconsolata,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
buildPackage {
 | 
					 | 
				
			||||||
  name = "foot";
 | 
					 | 
				
			||||||
  inherit (foot) version;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  app_id = 2;
 | 
					 | 
				
			||||||
  id = "org.codeberg.dnkl.foot";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  modules = [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      home.packages = [
 | 
					 | 
				
			||||||
        foot
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # For wayland-info:
 | 
					 | 
				
			||||||
        wayland-utils
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  nixosModules = [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      # To help with OCR:
 | 
					 | 
				
			||||||
      environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
 | 
					 | 
				
			||||||
        main = {
 | 
					 | 
				
			||||||
          font = "inconsolata:size=14";
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        colors = rec {
 | 
					 | 
				
			||||||
          foreground = "000000";
 | 
					 | 
				
			||||||
          background = "ffffff";
 | 
					 | 
				
			||||||
          regular2 = foreground;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fonts.packages = [ inconsolata ];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  script = ''
 | 
					 | 
				
			||||||
    exec foot "$@"
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,108 +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 fortify ps > '/tmp/{name}.ps'")
 | 
					 | 
				
			||||||
    machine.copy_from_vm(f"/tmp/{name}.ps", "")
 | 
					 | 
				
			||||||
    swaymsg(f"exec fortify --json ps > '/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 fortify --json ps"))
 | 
					 | 
				
			||||||
    if len(instances) != 1:
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected state length {len(instances)}")
 | 
					 | 
				
			||||||
    instance = next(iter(instances.values()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    config = instance['config']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['command'][0]):
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected command {instance['config']['command']}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if config['confinement']['enablements'] != enablements:
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
start_all()
 | 
					 | 
				
			||||||
machine.wait_for_unit("multi-user.target")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# To check fortify's version:
 | 
					 | 
				
			||||||
print(machine.succeed("sudo -u alice -i fortify version"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Wait for Sway to complete startup:
 | 
					 | 
				
			||||||
machine.wait_for_file("/run/user/1000/wayland-1")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/sway-ipc.sock")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Prepare fpkg directory:
 | 
					 | 
				
			||||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Install fpkg app:
 | 
					 | 
				
			||||||
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fpkg-install-done")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start app (foot) with Wayland enablement:
 | 
					 | 
				
			||||||
swaymsg("exec fpkg -v start org.codeberg.dnkl.foot")
 | 
					 | 
				
			||||||
wait_for_window("fortify@machine-foot")
 | 
					 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
 | 
					 | 
				
			||||||
collect_state_ui("app_wayland")
 | 
					 | 
				
			||||||
check_state("foot", 13)
 | 
					 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Exit Sway and verify process exit status 0:
 | 
					 | 
				
			||||||
swaymsg("exit", succeed=False)
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/sway-exit-ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Print fortify runDir contents:
 | 
					 | 
				
			||||||
print(machine.succeed("find /run/user/1000/fortify"))
 | 
					 | 
				
			||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,11 +10,10 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withNixDaemon(
 | 
					func withNixDaemon(
 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
						action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
				
			||||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
						app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
						fortifyAppDropShell(updateConfig(&fst.Config{
 | 
				
			||||||
		ID: app.ID,
 | 
							ID: app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
							Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
				
			||||||
			// start nix-daemon
 | 
								// start nix-daemon
 | 
				
			||||||
@ -58,11 +56,8 @@ func withNixDaemon(
 | 
				
			|||||||
	}), dropShell, beforeFail)
 | 
						}), dropShell, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withCacheDir(
 | 
					func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	ctx context.Context,
 | 
						fortifyAppDropShell(&fst.Config{
 | 
				
			||||||
	action string, command []string, workDir string,
 | 
					 | 
				
			||||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
					 | 
				
			||||||
	mustRunAppDropShell(ctx, &fst.Config{
 | 
					 | 
				
			||||||
		ID:      app.ID,
 | 
							ID:      app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
							Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
				
			||||||
		Confinement: fst.ConfinementConfig{
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
@ -95,12 +90,12 @@ func withCacheDir(
 | 
				
			|||||||
	}, dropShell, beforeFail)
 | 
						}, dropShell, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
 | 
					func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	if dropShell {
 | 
						if dropShell {
 | 
				
			||||||
		config.Command = []string{shellPath, "-l"}
 | 
							config.Command = []string{shellPath, "-l"}
 | 
				
			||||||
		mustRunApp(ctx, config, beforeFail)
 | 
							fortifyApp(config, beforeFail)
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		internal.Exit(0)
 | 
							internal.Exit(0)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	mustRunApp(ctx, config, beforeFail)
 | 
						fortifyApp(config, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
 | 
						compPoison  = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
				
			||||||
	fsuConfFile = "/etc/fsurc"
 | 
						fsuConfFile = "/etc/fsurc"
 | 
				
			||||||
	envShim     = "FORTIFY_SHIM"
 | 
						envShim     = "FORTIFY_SHIM"
 | 
				
			||||||
	envAID      = "FORTIFY_APP_ID"
 | 
						envAID      = "FORTIFY_APP_ID"
 | 
				
			||||||
@ -21,6 +22,10 @@ const (
 | 
				
			|||||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
						PR_SET_NO_NEW_PRIVS = 0x26
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						Fmain = compPoison
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	log.SetFlags(0)
 | 
						log.SetFlags(0)
 | 
				
			||||||
	log.SetPrefix("fsu: ")
 | 
						log.SetPrefix("fsu: ")
 | 
				
			||||||
@ -35,16 +40,20 @@ func main() {
 | 
				
			|||||||
		log.Fatal("this program must not be started by root")
 | 
							log.Fatal("this program must not be started by root")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var toolPath string
 | 
						var fmain string
 | 
				
			||||||
 | 
						if p, ok := checkPath(Fmain); !ok {
 | 
				
			||||||
 | 
							log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fmain = p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
 | 
						pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
 | 
				
			||||||
	if p, err := os.Readlink(pexe); err != nil {
 | 
						if p, err := os.Readlink(pexe); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot read parent executable path: %v", err)
 | 
							log.Fatalf("cannot read parent executable path: %v", err)
 | 
				
			||||||
	} else if strings.HasSuffix(p, " (deleted)") {
 | 
						} else if strings.HasSuffix(p, " (deleted)") {
 | 
				
			||||||
		log.Fatal("fortify executable has been deleted")
 | 
							log.Fatal("fortify executable has been deleted")
 | 
				
			||||||
	} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
 | 
						} else if p != fmain {
 | 
				
			||||||
		log.Fatal("this program must be started by fortify")
 | 
							log.Fatal("this program must be started by fortify")
 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		toolPath = p
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// uid = 1000000 +
 | 
						// uid = 1000000 +
 | 
				
			||||||
@ -138,9 +147,13 @@ func main() {
 | 
				
			|||||||
	if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
 | 
						if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
 | 
				
			||||||
		log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
 | 
							log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := syscall.Exec(toolPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
 | 
						if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot start shim: %v", err)
 | 
							log.Fatalf("cannot start shim: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	panic("unreachable")
 | 
						panic("unreachable")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func checkPath(p string) (string, bool) {
 | 
				
			||||||
 | 
						return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					 | 
				
			||||||
  buildGoModule,
 | 
					  buildGoModule,
 | 
				
			||||||
  fortify ? abort "fortify package required",
 | 
					  fortify ? abort "fortify package required",
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
@ -16,15 +15,5 @@ buildGoModule {
 | 
				
			|||||||
    go mod init fsu >& /dev/null
 | 
					    go mod init fsu >& /dev/null
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ldflags =
 | 
					  ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
 | 
				
			||||||
    lib.attrsets.foldlAttrs
 | 
					 | 
				
			||||||
      (
 | 
					 | 
				
			||||||
        ldflags: name: value:
 | 
					 | 
				
			||||||
        ldflags ++ [ "-X main.${name}=${value}" ]
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      [ "-s -w" ]
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        fmain = "${fortify}/libexec/fortify";
 | 
					 | 
				
			||||||
        fpkg = "${fortify}/libexec/fpkg";
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	fmain = compPoison
 | 
					 | 
				
			||||||
	fpkg  = compPoison
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func mustCheckPath(p string) string {
 | 
					 | 
				
			||||||
	if p != compPoison && p != "" && path.IsAbs(p) {
 | 
					 | 
				
			||||||
		return p
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	log.Fatal("this program is compiled incorrectly")
 | 
					 | 
				
			||||||
	return compPoison
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -110,7 +110,7 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
 | 
				
			|||||||
			bc.Bind(k, k)
 | 
								bc.Bind(k, k)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h = helper.MustNewBwrap(bc, toolPath, true, p.seal, argF, nil, nil)
 | 
							h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
 | 
				
			||||||
		p.bwrap = bc
 | 
							p.bwrap = bc
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							@ -10,10 +10,9 @@ cp -rv "comp" "${out}"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
go generate ./...
 | 
					go generate ./...
 | 
				
			||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
 | 
					go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
 | 
				
			||||||
  -X git.gensokyo.uk/security/fortify/internal.version=${VERSION}
 | 
					  -X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
 | 
				
			||||||
  -X git.gensokyo.uk/security/fortify/internal.fsu=/usr/bin/fsu
 | 
					  -X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
 | 
				
			||||||
  -X main.fmain=/usr/bin/fortify
 | 
					  -X main.Fmain=/usr/bin/fortify" ./...
 | 
				
			||||||
  -X main.fpkg=/usr/bin/fpkg" ./...
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
 | 
					rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
 | 
				
			||||||
rm -rf "./${out}"
 | 
					rm -rf "./${out}"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										46
									
								
								error.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								error.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func logWaitError(err error) {
 | 
				
			||||||
 | 
						var e *fmsg.BaseError
 | 
				
			||||||
 | 
						if !fmsg.AsBaseError(err, &e) {
 | 
				
			||||||
 | 
							log.Println("wait failed:", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
 | 
				
			||||||
 | 
							var se *app.StateStoreError
 | 
				
			||||||
 | 
							if !errors.As(err, &se) {
 | 
				
			||||||
 | 
								// does not need special handling
 | 
				
			||||||
 | 
								log.Print(e.Message())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// inner error are either unwrapped store errors
 | 
				
			||||||
 | 
								// or joined errors returned by *appSealTx revert
 | 
				
			||||||
 | 
								// wrapped in *app.BaseError
 | 
				
			||||||
 | 
								var ej app.RevertCompoundError
 | 
				
			||||||
 | 
								if !errors.As(se.InnerErr, &ej) {
 | 
				
			||||||
 | 
									// does not require special handling
 | 
				
			||||||
 | 
									log.Print(e.Message())
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									errs := ej.Unwrap()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// every error here is wrapped in *app.BaseError
 | 
				
			||||||
 | 
									for _, ei := range errs {
 | 
				
			||||||
 | 
										var eb *fmsg.BaseError
 | 
				
			||||||
 | 
										if !errors.As(ei, &eb) {
 | 
				
			||||||
 | 
											// unreachable
 | 
				
			||||||
 | 
											log.Println("invalid error type returned by revert:", ei)
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											// print inner *app.BaseError message
 | 
				
			||||||
 | 
											log.Print(eb.Message())
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								flake.nix
									
									
									
									
									
								
							@ -58,7 +58,6 @@
 | 
				
			|||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          fortify = callPackage ./test { inherit system self; };
 | 
					          fortify = callPackage ./test { inherit system self; };
 | 
				
			||||||
          fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
 | 
					 | 
				
			||||||
          race = callPackage ./test {
 | 
					          race = callPackage ./test {
 | 
				
			||||||
            inherit system self;
 | 
					            inherit system self;
 | 
				
			||||||
            withRace = true;
 | 
					            withRace = true;
 | 
				
			||||||
@ -68,7 +67,7 @@
 | 
				
			|||||||
            cd ${./.}
 | 
					            cd ${./.}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            echo "running nixfmt..."
 | 
					            echo "running nixfmt..."
 | 
				
			||||||
            nixfmt --width=256 --check .
 | 
					            nixfmt --check .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            touch $out
 | 
					            touch $out
 | 
				
			||||||
          '';
 | 
					          '';
 | 
				
			||||||
@ -98,32 +97,24 @@
 | 
				
			|||||||
      packages = forAllSystems (
 | 
					      packages = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
        let
 | 
					        let
 | 
				
			||||||
          inherit (self.packages.${system}) fortify fsu;
 | 
					          inherit (self.packages.${system}) fortify;
 | 
				
			||||||
          pkgs = nixpkgsFor.${system};
 | 
					          pkgs = nixpkgsFor.${system};
 | 
				
			||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          default = fortify;
 | 
					          default = self.packages.${system}.fortify;
 | 
				
			||||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
				
			||||||
            inherit (pkgs)
 | 
					            inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
				
			||||||
              bubblewrap
 | 
					 | 
				
			||||||
              xdg-dbus-proxy
 | 
					 | 
				
			||||||
              glibc
 | 
					 | 
				
			||||||
              zstd
 | 
					 | 
				
			||||||
              gnutar
 | 
					 | 
				
			||||||
              coreutils
 | 
					 | 
				
			||||||
              ;
 | 
					 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
					          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
 | 
					          dist =
 | 
				
			||||||
 | 
					            pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
 | 
				
			||||||
 | 
					              ''
 | 
				
			||||||
                # go requires XDG_CACHE_HOME for the build cache
 | 
					                # go requires XDG_CACHE_HOME for the build cache
 | 
				
			||||||
                export XDG_CACHE_HOME="$(mktemp -d)"
 | 
					                export XDG_CACHE_HOME="$(mktemp -d)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # get a different workdir as go does not like /build
 | 
					                # get a different workdir as go does not like /build
 | 
				
			||||||
            cd $(mktemp -d) \
 | 
					                cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
 | 
				
			||||||
                && cp -r ${fortify.src}/. . \
 | 
					 | 
				
			||||||
                && chmod +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
 | 
					 | 
				
			||||||
                && chmod -R +w .
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                export FORTIFY_VERSION="v${fortify.version}"
 | 
					                export FORTIFY_VERSION="v${fortify.version}"
 | 
				
			||||||
                ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
					                ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
@ -24,9 +23,6 @@ type bubblewrap struct {
 | 
				
			|||||||
	// name of the command to run in bwrap
 | 
						// name of the command to run in bwrap
 | 
				
			||||||
	name string
 | 
						name string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// whether to set process group id
 | 
					 | 
				
			||||||
	setpgid bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	lock sync.RWMutex
 | 
						lock sync.RWMutex
 | 
				
			||||||
	*helperCmd
 | 
						*helperCmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -42,10 +38,6 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	args := b.finalise(ctx, stat)
 | 
						args := b.finalise(ctx, stat)
 | 
				
			||||||
	if b.setpgid {
 | 
					 | 
				
			||||||
		b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
 | 
						b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
 | 
				
			||||||
	b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
 | 
						b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
 | 
				
			||||||
	b.Cmd.Args = append(b.Cmd.Args, args...)
 | 
						b.Cmd.Args = append(b.Cmd.Args, args...)
 | 
				
			||||||
@ -56,12 +48,12 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
 | 
				
			|||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
					// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
func MustNewBwrap(
 | 
					func MustNewBwrap(
 | 
				
			||||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
						conf *bwrap.Config, name string,
 | 
				
			||||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
						wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
				
			||||||
	extraFiles []*os.File,
 | 
						extraFiles []*os.File,
 | 
				
			||||||
	syncFd *os.File,
 | 
						syncFd *os.File,
 | 
				
			||||||
) Helper {
 | 
					) Helper {
 | 
				
			||||||
	b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
 | 
						b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		panic(err.Error())
 | 
							panic(err.Error())
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -73,7 +65,7 @@ func MustNewBwrap(
 | 
				
			|||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
					// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
func NewBwrap(
 | 
					func NewBwrap(
 | 
				
			||||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
						conf *bwrap.Config, name string,
 | 
				
			||||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
						wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
				
			||||||
	extraFiles []*os.File,
 | 
						extraFiles []*os.File,
 | 
				
			||||||
	syncFd *os.File,
 | 
						syncFd *os.File,
 | 
				
			||||||
@ -81,7 +73,6 @@ func NewBwrap(
 | 
				
			|||||||
	b := new(bubblewrap)
 | 
						b := new(bubblewrap)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	b.name = name
 | 
						b.name = name
 | 
				
			||||||
	b.setpgid = setpgid
 | 
					 | 
				
			||||||
	b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
 | 
						b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
 | 
						if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
							h := helper.MustNewBwrap(
 | 
				
			||||||
			sc, "fortify", false,
 | 
								sc, "fortify",
 | 
				
			||||||
			argsWt, argF,
 | 
								argsWt, argF,
 | 
				
			||||||
			nil, nil,
 | 
								nil, nil,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
@ -44,7 +44,7 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
		if got := helper.MustNewBwrap(
 | 
							if got := helper.MustNewBwrap(
 | 
				
			||||||
			sc, "fortify", false,
 | 
								sc, "fortify",
 | 
				
			||||||
			argsWt, argF,
 | 
								argsWt, argF,
 | 
				
			||||||
			nil, nil,
 | 
								nil, nil,
 | 
				
			||||||
		); got == nil {
 | 
							); got == nil {
 | 
				
			||||||
@ -64,7 +64,7 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
		}()
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		helper.MustNewBwrap(
 | 
							helper.MustNewBwrap(
 | 
				
			||||||
			&bwrap.Config{Hostname: "\x00"}, "fortify", false,
 | 
								&bwrap.Config{Hostname: "\x00"}, "fortify",
 | 
				
			||||||
			nil, argF,
 | 
								nil, argF,
 | 
				
			||||||
			nil, nil,
 | 
								nil, nil,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
@ -74,7 +74,7 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
		helper.InternalReplaceExecCommand(t)
 | 
							helper.InternalReplaceExecCommand(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
							h := helper.MustNewBwrap(
 | 
				
			||||||
			sc, "crash-test-dummy", false,
 | 
								sc, "crash-test-dummy",
 | 
				
			||||||
			nil, argFChecked,
 | 
								nil, argFChecked,
 | 
				
			||||||
			nil, nil,
 | 
								nil, nil,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
@ -98,11 +98,6 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
		testHelper(t, func() helper.Helper {
 | 
							testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
 | 
				
			||||||
			return helper.MustNewBwrap(
 | 
					 | 
				
			||||||
				sc, "crash-test-dummy", false,
 | 
					 | 
				
			||||||
				argsWt, argF, nil, nil,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,179 +0,0 @@
 | 
				
			|||||||
package app
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func PrintRunStateErr(rs *fst.RunState, runErr error) {
 | 
					 | 
				
			||||||
	if runErr != nil {
 | 
					 | 
				
			||||||
		if rs.Time == nil {
 | 
					 | 
				
			||||||
			fmsg.PrintBaseError(runErr, "cannot start app:")
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			var e *fmsg.BaseError
 | 
					 | 
				
			||||||
			if !fmsg.AsBaseError(runErr, &e) {
 | 
					 | 
				
			||||||
				log.Println("wait failed:", runErr)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
 | 
					 | 
				
			||||||
				var se *StateStoreError
 | 
					 | 
				
			||||||
				if !errors.As(runErr, &se) {
 | 
					 | 
				
			||||||
					// does not need special handling
 | 
					 | 
				
			||||||
					log.Print(e.Message())
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					// inner error are either unwrapped store errors
 | 
					 | 
				
			||||||
					// or joined errors returned by *appSealTx revert
 | 
					 | 
				
			||||||
					// wrapped in *app.BaseError
 | 
					 | 
				
			||||||
					var ej RevertCompoundError
 | 
					 | 
				
			||||||
					if !errors.As(se.InnerErr, &ej) {
 | 
					 | 
				
			||||||
						// does not require special handling
 | 
					 | 
				
			||||||
						log.Print(e.Message())
 | 
					 | 
				
			||||||
					} else {
 | 
					 | 
				
			||||||
						errs := ej.Unwrap()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						// every error here is wrapped in *app.BaseError
 | 
					 | 
				
			||||||
						for _, ei := range errs {
 | 
					 | 
				
			||||||
							var eb *fmsg.BaseError
 | 
					 | 
				
			||||||
							if !errors.As(ei, &eb) {
 | 
					 | 
				
			||||||
								// unreachable
 | 
					 | 
				
			||||||
								log.Println("invalid error type returned by revert:", ei)
 | 
					 | 
				
			||||||
							} else {
 | 
					 | 
				
			||||||
								// print inner *app.BaseError message
 | 
					 | 
				
			||||||
								log.Print(eb.Message())
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if rs.ExitCode == 0 {
 | 
					 | 
				
			||||||
			rs.ExitCode = 126
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if rs.RevertErr != nil {
 | 
					 | 
				
			||||||
		var stateStoreError *StateStoreError
 | 
					 | 
				
			||||||
		if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
 | 
					 | 
				
			||||||
			fmsg.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
 | 
					 | 
				
			||||||
			goto out
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if stateStoreError.Err != nil {
 | 
					 | 
				
			||||||
			if len(stateStoreError.Err) == 2 {
 | 
					 | 
				
			||||||
				if stateStoreError.Err[0] != nil {
 | 
					 | 
				
			||||||
					if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
 | 
					 | 
				
			||||||
						fmsg.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
 | 
					 | 
				
			||||||
					} else {
 | 
					 | 
				
			||||||
						for _, err := range joinedErrs.Unwrap() {
 | 
					 | 
				
			||||||
							if err != nil {
 | 
					 | 
				
			||||||
								fmsg.PrintBaseError(err, "fault during revert:")
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if stateStoreError.Err[1] != nil {
 | 
					 | 
				
			||||||
					log.Printf("cannot close store: %v", stateStoreError.Err[1])
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				log.Printf("fault during cleanup: %v",
 | 
					 | 
				
			||||||
					errors.Join(stateStoreError.Err...))
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if stateStoreError.OpErr != nil {
 | 
					 | 
				
			||||||
			log.Printf("blind revert due to store fault: %v",
 | 
					 | 
				
			||||||
				stateStoreError.OpErr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if stateStoreError.DoErr != nil {
 | 
					 | 
				
			||||||
			fmsg.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if stateStoreError.Inner && stateStoreError.InnerErr != nil {
 | 
					 | 
				
			||||||
			fmsg.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out:
 | 
					 | 
				
			||||||
		if rs.ExitCode == 0 {
 | 
					 | 
				
			||||||
			rs.ExitCode = 128
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if rs.WaitErr != nil {
 | 
					 | 
				
			||||||
		log.Println("inner wait failed:", rs.WaitErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// StateStoreError is returned for a failed state save
 | 
					 | 
				
			||||||
type StateStoreError struct {
 | 
					 | 
				
			||||||
	// whether inner function was called
 | 
					 | 
				
			||||||
	Inner bool
 | 
					 | 
				
			||||||
	// returned by the Save/Destroy method of [state.Cursor]
 | 
					 | 
				
			||||||
	InnerErr error
 | 
					 | 
				
			||||||
	// returned by the Do method of [state.Store]
 | 
					 | 
				
			||||||
	DoErr error
 | 
					 | 
				
			||||||
	// stores an arbitrary store operation error
 | 
					 | 
				
			||||||
	OpErr error
 | 
					 | 
				
			||||||
	// stores arbitrary errors
 | 
					 | 
				
			||||||
	Err []error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// save saves arbitrary errors in [StateStoreError] once.
 | 
					 | 
				
			||||||
func (e *StateStoreError) save(errs []error) {
 | 
					 | 
				
			||||||
	if len(errs) == 0 || e.Err != nil {
 | 
					 | 
				
			||||||
		panic("invalid call to save")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	e.Err = errs
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) equiv(a ...any) error {
 | 
					 | 
				
			||||||
	if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return fmsg.WrapErrorSuffix(e, a...)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) Error() string {
 | 
					 | 
				
			||||||
	if e.Inner && e.InnerErr != nil {
 | 
					 | 
				
			||||||
		return e.InnerErr.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.DoErr != nil {
 | 
					 | 
				
			||||||
		return e.DoErr.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.OpErr != nil {
 | 
					 | 
				
			||||||
		return e.OpErr.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := errors.Join(e.Err...); err != nil {
 | 
					 | 
				
			||||||
		return err.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// equiv nullifies e for values where this is reached
 | 
					 | 
				
			||||||
	panic("unreachable")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) Unwrap() (errs []error) {
 | 
					 | 
				
			||||||
	errs = make([]error, 0, 3)
 | 
					 | 
				
			||||||
	if e.InnerErr != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.InnerErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.DoErr != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.DoErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.OpErr != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.OpErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := errors.Join(e.Err...); err != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// A RevertCompoundError encapsulates errors returned by
 | 
					 | 
				
			||||||
// the Revert method of [system.I].
 | 
					 | 
				
			||||||
type RevertCompoundError interface {
 | 
					 | 
				
			||||||
	Error() string
 | 
					 | 
				
			||||||
	Unwrap() []error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -12,7 +12,6 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
						"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
@ -33,10 +32,6 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		panic("invalid state")
 | 
							panic("invalid state")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// read comp values early to allow for early failure
 | 
					 | 
				
			||||||
	fmsg.Verbosef("version %s", internal.Version())
 | 
					 | 
				
			||||||
	fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		resolve exec paths
 | 
							resolve exec paths
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
@ -84,8 +79,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
				ec.Set(system.Process)
 | 
									ec.Set(system.Process)
 | 
				
			||||||
				if states, err := c.Load(); err != nil {
 | 
									if states, err := c.Load(); err != nil {
 | 
				
			||||||
					// revert per-process state here to limit damage
 | 
										// revert per-process state here to limit damage
 | 
				
			||||||
					storeErr.OpErr = err
 | 
										return errors.Join(err, seal.sys.Revert(ec))
 | 
				
			||||||
					return seal.sys.Revert(ec)
 | 
					 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					if l := len(states); l == 0 {
 | 
										if l := len(states); l == 0 {
 | 
				
			||||||
						fmsg.Verbose("no other launchers active, will clean up globals")
 | 
											fmsg.Verbose("no other launchers active, will clean up globals")
 | 
				
			||||||
@ -121,10 +115,14 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return seal.sys.Revert(ec)
 | 
									err := seal.sys.Revert(ec)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										err = err.(RevertCompoundError)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
			}()
 | 
								}()
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		storeErr.save([]error{revertErr, store.Close()})
 | 
							storeErr.Err = errors.Join(revertErr, store.Close())
 | 
				
			||||||
		rs.RevertErr = storeErr.equiv("error returned during cleanup:")
 | 
							rs.RevertErr = storeErr.equiv("error returned during cleanup:")
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -172,9 +170,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		Time: *rs.Time,
 | 
							Time: *rs.Time,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
 | 
						var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
 | 
				
			||||||
	earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
 | 
						earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) })
 | 
				
			||||||
		earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	// destroy defunct state entry
 | 
						// destroy defunct state entry
 | 
				
			||||||
	deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
 | 
						deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -216,3 +212,69 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return earlyStoreErr.equiv("cannot save process state:")
 | 
						return earlyStoreErr.equiv("cannot save process state:")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StateStoreError is returned for a failed state save
 | 
				
			||||||
 | 
					type StateStoreError struct {
 | 
				
			||||||
 | 
						// whether inner function was called
 | 
				
			||||||
 | 
						Inner bool
 | 
				
			||||||
 | 
						// returned by the Do method of [state.Store]
 | 
				
			||||||
 | 
						DoErr error
 | 
				
			||||||
 | 
						// returned by the Save/Destroy method of [state.Cursor]
 | 
				
			||||||
 | 
						InnerErr error
 | 
				
			||||||
 | 
						// stores an arbitrary error
 | 
				
			||||||
 | 
						Err error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// save saves exactly one arbitrary error in [StateStoreError].
 | 
				
			||||||
 | 
					func (e *StateStoreError) save(err error) {
 | 
				
			||||||
 | 
						if err == nil || e.Err != nil {
 | 
				
			||||||
 | 
							panic("invalid call to save")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						e.Err = err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *StateStoreError) equiv(a ...any) error {
 | 
				
			||||||
 | 
						if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return fmsg.WrapErrorSuffix(e, a...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *StateStoreError) Error() string {
 | 
				
			||||||
 | 
						if e.Inner && e.InnerErr != nil {
 | 
				
			||||||
 | 
							return e.InnerErr.Error()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if e.DoErr != nil {
 | 
				
			||||||
 | 
							return e.DoErr.Error()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if e.Err != nil {
 | 
				
			||||||
 | 
							return e.Err.Error()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// equiv nullifies e for values where this is reached
 | 
				
			||||||
 | 
						panic("unreachable")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *StateStoreError) Unwrap() (errs []error) {
 | 
				
			||||||
 | 
						errs = make([]error, 0, 3)
 | 
				
			||||||
 | 
						if e.DoErr != nil {
 | 
				
			||||||
 | 
							errs = append(errs, e.DoErr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if e.InnerErr != nil {
 | 
				
			||||||
 | 
							errs = append(errs, e.InnerErr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if e.Err != nil {
 | 
				
			||||||
 | 
							errs = append(errs, e.Err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A RevertCompoundError encapsulates errors returned by
 | 
				
			||||||
 | 
					// the Revert method of [system.I].
 | 
				
			||||||
 | 
					type RevertCompoundError interface {
 | 
				
			||||||
 | 
						Error() string
 | 
				
			||||||
 | 
						Unwrap() []error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -125,7 +125,7 @@ func Main() {
 | 
				
			|||||||
		seccomp.CPrintln = log.Println
 | 
							seccomp.CPrintln = log.Println
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if b, err := helper.NewBwrap(
 | 
						if b, err := helper.NewBwrap(
 | 
				
			||||||
		conf, path.Join(fst.Tmp, "sbin/init"), false,
 | 
							conf, path.Join(fst.Tmp, "sbin/init"),
 | 
				
			||||||
		nil, func(int, int) []string { return make([]string, 0) },
 | 
							nil, func(int, int) []string { return make([]string, 0) },
 | 
				
			||||||
		extraFiles,
 | 
							extraFiles,
 | 
				
			||||||
		syncFd,
 | 
							syncFd,
 | 
				
			||||||
 | 
				
			|||||||
@ -52,8 +52,14 @@ func (s *Shim) Start(
 | 
				
			|||||||
	syncFd *os.File,
 | 
						syncFd *os.File,
 | 
				
			||||||
) (*time.Time, error) {
 | 
					) (*time.Time, error) {
 | 
				
			||||||
	// prepare user switcher invocation
 | 
						// prepare user switcher invocation
 | 
				
			||||||
	fsuPath := internal.MustFsuPath()
 | 
						var fsu string
 | 
				
			||||||
	s.cmd = exec.Command(fsuPath)
 | 
						if p, ok := internal.Path(internal.Fsu); !ok {
 | 
				
			||||||
 | 
							return nil, fmsg.WrapError(errors.New("bad fsu path"),
 | 
				
			||||||
 | 
								"invalid fsu path, this copy of fortify is not compiled correctly")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fsu = p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						s.cmd = exec.Command(fsu)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass shim setup pipe
 | 
						// pass shim setup pipe
 | 
				
			||||||
	if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
						if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,15 +3,10 @@ package internal
 | 
				
			|||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
					const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	version = compPoison
 | 
						Version = compPoison
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// check validates string value set at compile time.
 | 
					// Check validates string value set at compile time.
 | 
				
			||||||
func check(s string) (string, bool) { return s, s != compPoison && s != "" }
 | 
					func Check(s string) (string, bool) {
 | 
				
			||||||
 | 
						return s, s != compPoison && s != ""
 | 
				
			||||||
func Version() string {
 | 
					 | 
				
			||||||
	if v, ok := check(version); ok {
 | 
					 | 
				
			||||||
		return v
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return "impure"
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +1,11 @@
 | 
				
			|||||||
package internal
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import "path"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	fsu = compPoison
 | 
						Fsu = compPoison
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustFsuPath() string {
 | 
					func Path(p string) (string, bool) {
 | 
				
			||||||
	if name, ok := checkPath(fsu); ok {
 | 
						return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
				
			||||||
		return name
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fmsg.BeforeExit()
 | 
					 | 
				
			||||||
	log.Fatal("invalid fsu path, this program is compiled incorrectly")
 | 
					 | 
				
			||||||
	return compPoison
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"os/user"
 | 
						"os/user"
 | 
				
			||||||
@ -78,10 +79,14 @@ func (s *Std) Uid(aid int) (int, error) {
 | 
				
			|||||||
	defer func() { s.uidCopy[aid] = u }()
 | 
						defer func() { s.uidCopy[aid] = u }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u.uid = -1
 | 
						u.uid = -1
 | 
				
			||||||
	fsuPath := internal.MustFsuPath()
 | 
						if fsu, ok := internal.Check(internal.Fsu); !ok {
 | 
				
			||||||
 | 
							fmsg.BeforeExit()
 | 
				
			||||||
	cmd := exec.Command(fsuPath)
 | 
							log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
 | 
				
			||||||
	cmd.Path = fsuPath
 | 
							// unreachable
 | 
				
			||||||
 | 
							return 0, syscall.EBADE
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							cmd := exec.Command(fsu)
 | 
				
			||||||
 | 
							cmd.Path = fsu
 | 
				
			||||||
		cmd.Stderr = os.Stderr // pass through fatal messages
 | 
							cmd.Stderr = os.Stderr // pass through fatal messages
 | 
				
			||||||
		cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
 | 
							cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
 | 
				
			||||||
		cmd.Dir = "/"
 | 
							cmd.Dir = "/"
 | 
				
			||||||
@ -98,7 +103,8 @@ func (s *Std) Uid(aid int) (int, error) {
 | 
				
			|||||||
		} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
 | 
							} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
 | 
				
			||||||
			u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
 | 
								u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
 | 
				
			||||||
		} else if os.IsNotExist(u.err) {
 | 
							} else if os.IsNotExist(u.err) {
 | 
				
			||||||
		u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsuPath))
 | 
								u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsu))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return u.uid, u.err
 | 
							return u.uid, u.err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,7 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
 | 
				
			|||||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
								Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
				
			||||||
			NewSession:    true,
 | 
								NewSession:    true,
 | 
				
			||||||
			DieWithParent: true,
 | 
								DieWithParent: true,
 | 
				
			||||||
		}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
 | 
							}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
 | 
				
			||||||
		nil, func(_, _ int) []string { return []string{p} },
 | 
							nil, func(_, _ int) []string { return []string{p} },
 | 
				
			||||||
		nil, nil,
 | 
							nil, nil,
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										50
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								main.go
									
									
									
									
									
								
							@ -68,20 +68,10 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		flagVerbose bool
 | 
							flagVerbose bool
 | 
				
			||||||
		flagJSON    bool
 | 
							flagJSON    bool
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	c := command.New(out, log.Printf, "fortify", func([]string) error {
 | 
						c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
 | 
				
			||||||
		fmsg.Store(flagVerbose)
 | 
					 | 
				
			||||||
		if flagVerbose {
 | 
					 | 
				
			||||||
			seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}).
 | 
					 | 
				
			||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
							Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// internal commands
 | 
					 | 
				
			||||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
					 | 
				
			||||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
						c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
				
			||||||
		if len(args) < 1 {
 | 
							if len(args) < 1 {
 | 
				
			||||||
			log.Fatal("app requires at least 1 argument")
 | 
								log.Fatal("app requires at least 1 argument")
 | 
				
			||||||
@ -259,7 +249,11 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
	}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
 | 
						}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Command("version", "Show fortify version", func(args []string) error {
 | 
						c.Command("version", "Show fortify version", func(args []string) error {
 | 
				
			||||||
		fmt.Println(internal.Version())
 | 
							if v, ok := internal.Check(internal.Version); ok {
 | 
				
			||||||
 | 
								fmt.Println(v)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								fmt.Println("impure")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return errSuccess
 | 
							return errSuccess
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -278,21 +272,45 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		return errSuccess
 | 
							return errSuccess
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// internal commands
 | 
				
			||||||
 | 
						c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
				
			||||||
 | 
						c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return c
 | 
						return c
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func runApp(a fst.App, config *fst.Config) {
 | 
					func runApp(a fst.App, config *fst.Config) {
 | 
				
			||||||
 | 
						rs := new(fst.RunState)
 | 
				
			||||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
						ctx, stop := signal.NotifyContext(context.Background(),
 | 
				
			||||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
							syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
	defer stop() // unreachable
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rs := new(fst.RunState)
 | 
						if fmsg.Load() {
 | 
				
			||||||
 | 
							seccomp.CPrintln = log.Println
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if sa, err := a.Seal(config); err != nil {
 | 
						if sa, err := a.Seal(config); err != nil {
 | 
				
			||||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
							fmsg.PrintBaseError(err, "cannot seal app:")
 | 
				
			||||||
		rs.ExitCode = 1
 | 
							internal.Exit(1)
 | 
				
			||||||
 | 
						} else if err = sa.Run(ctx, rs); err != nil {
 | 
				
			||||||
 | 
							if rs.Time == nil {
 | 
				
			||||||
 | 
								fmsg.PrintBaseError(err, "cannot start app:")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
		// this updates ExitCode
 | 
								logWaitError(err)
 | 
				
			||||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if rs.ExitCode == 0 {
 | 
				
			||||||
 | 
								rs.ExitCode = 126
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rs.RevertErr != nil {
 | 
				
			||||||
 | 
							fmsg.PrintBaseError(rs.RevertErr, "generic error returned during cleanup:")
 | 
				
			||||||
 | 
							if rs.ExitCode == 0 {
 | 
				
			||||||
 | 
								rs.ExitCode = 128
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rs.WaitErr != nil {
 | 
				
			||||||
 | 
							log.Println("inner wait failed:", rs.WaitErr)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	internal.Exit(rs.ExitCode)
 | 
						internal.Exit(rs.ExitCode)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								nixos.nix
									
									
									
									
									
								
							@ -77,12 +77,21 @@ in
 | 
				
			|||||||
                      };
 | 
					                      };
 | 
				
			||||||
                    in
 | 
					                    in
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                      session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault app.id)) else (extendDBusDefault app.id default);
 | 
					                      session_bus =
 | 
				
			||||||
 | 
					                        if app.dbus.session != null then
 | 
				
			||||||
 | 
					                          (app.dbus.session (extendDBusDefault app.id))
 | 
				
			||||||
 | 
					                        else
 | 
				
			||||||
 | 
					                          (extendDBusDefault app.id default);
 | 
				
			||||||
                      system_bus = app.dbus.system;
 | 
					                      system_bus = app.dbus.system;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                  command = if app.command == null then app.name else app.command;
 | 
					                  command = if app.command == null then app.name else app.command;
 | 
				
			||||||
                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
					                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
				
			||||||
                  enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
 | 
					                  enablements =
 | 
				
			||||||
 | 
					                    with app.capability;
 | 
				
			||||||
 | 
					                    (if wayland then 1 else 0)
 | 
				
			||||||
 | 
					                    + (if x11 then 2 else 0)
 | 
				
			||||||
 | 
					                    + (if dbus then 4 else 0)
 | 
				
			||||||
 | 
					                    + (if pulse then 8 else 0);
 | 
				
			||||||
                  conf = {
 | 
					                  conf = {
 | 
				
			||||||
                    inherit (app) id;
 | 
					                    inherit (app) id;
 | 
				
			||||||
                    command = [
 | 
					                    command = [
 | 
				
			||||||
@ -156,7 +165,9 @@ in
 | 
				
			|||||||
                  };
 | 
					                  };
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
                pkgs.writeShellScriptBin app.name ''
 | 
					                pkgs.writeShellScriptBin app.name ''
 | 
				
			||||||
                  exec fortify${if app.verbose then " -v" else ""} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
					                  exec fortify${
 | 
				
			||||||
 | 
					                    if app.verbose then " -v" else ""
 | 
				
			||||||
 | 
					                  } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
				
			||||||
                ''
 | 
					                ''
 | 
				
			||||||
              ) cfg.apps;
 | 
					              ) cfg.apps;
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
 | 
				
			|||||||
@ -3,14 +3,7 @@
 | 
				
			|||||||
let
 | 
					let
 | 
				
			||||||
  inherit (lib) types mkOption mkEnableOption;
 | 
					  inherit (lib) types mkOption mkEnableOption;
 | 
				
			||||||
  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
				
			||||||
    inherit (pkgs)
 | 
					    inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
				
			||||||
      bubblewrap
 | 
					 | 
				
			||||||
      xdg-dbus-proxy
 | 
					 | 
				
			||||||
      glibc
 | 
					 | 
				
			||||||
      zstd
 | 
					 | 
				
			||||||
      gnutar
 | 
					 | 
				
			||||||
      coreutils
 | 
					 | 
				
			||||||
      ;
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										42
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.nix
									
									
									
									
									
								
							@ -14,11 +14,6 @@
 | 
				
			|||||||
  wayland-scanner,
 | 
					  wayland-scanner,
 | 
				
			||||||
  xorg,
 | 
					  xorg,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # for fpkg
 | 
					 | 
				
			||||||
  zstd,
 | 
					 | 
				
			||||||
  gnutar,
 | 
					 | 
				
			||||||
  coreutils,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  glibc, # for ldd
 | 
					  glibc, # for ldd
 | 
				
			||||||
  withStatic ? stdenv.hostPlatform.isStatic,
 | 
					  withStatic ? stdenv.hostPlatform.isStatic,
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
@ -30,7 +25,10 @@ buildGoModule rec {
 | 
				
			|||||||
  src = builtins.path {
 | 
					  src = builtins.path {
 | 
				
			||||||
    name = "${pname}-src";
 | 
					    name = "${pname}-src";
 | 
				
			||||||
    path = lib.cleanSource ./.;
 | 
					    path = lib.cleanSource ./.;
 | 
				
			||||||
    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
					    filter =
 | 
				
			||||||
 | 
					      path: type:
 | 
				
			||||||
 | 
					      !(type == "regular" && lib.hasSuffix ".nix" path)
 | 
				
			||||||
 | 
					      && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  vendorHash = null;
 | 
					  vendorHash = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,15 +39,17 @@ buildGoModule rec {
 | 
				
			|||||||
        ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
 | 
					        ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      (
 | 
					      (
 | 
				
			||||||
        [ "-s -w" ]
 | 
					        [
 | 
				
			||||||
 | 
					          "-s -w"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
        ++ lib.optionals withStatic [
 | 
					        ++ lib.optionals withStatic [
 | 
				
			||||||
          "-linkmode external"
 | 
					          "-linkmode external"
 | 
				
			||||||
          "-extldflags \"-static\""
 | 
					          "-extldflags \"-static\""
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        version = "v${version}";
 | 
					        Version = "v${version}";
 | 
				
			||||||
        fsu = "/run/wrappers/bin/fsu";
 | 
					        Fsu = "/run/wrappers/bin/fsu";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # nix build environment does not allow acls
 | 
					  # nix build environment does not allow acls
 | 
				
			||||||
@ -79,33 +79,19 @@ buildGoModule rec {
 | 
				
			|||||||
    HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
 | 
					    HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  postInstall =
 | 
					  postInstall = ''
 | 
				
			||||||
    let
 | 
					 | 
				
			||||||
      appPackages = [
 | 
					 | 
				
			||||||
        glibc
 | 
					 | 
				
			||||||
        bubblewrap
 | 
					 | 
				
			||||||
        xdg-dbus-proxy
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
    in
 | 
					 | 
				
			||||||
    ''
 | 
					 | 
				
			||||||
    install -D --target-directory=$out/share/zsh/site-functions comp/*
 | 
					    install -D --target-directory=$out/share/zsh/site-functions comp/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mkdir "$out/libexec"
 | 
					    mkdir "$out/libexec"
 | 
				
			||||||
    mv "$out"/bin/* "$out/libexec/"
 | 
					    mv "$out"/bin/* "$out/libexec/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
 | 
					    makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
 | 
				
			||||||
        --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      makeBinaryWrapper "$out/libexec/fpkg" "$out/bin/fpkg" \
 | 
					 | 
				
			||||||
      --inherit-argv0 --prefix PATH : ${
 | 
					      --inherit-argv0 --prefix PATH : ${
 | 
				
			||||||
          lib.makeBinPath (
 | 
					        lib.makeBinPath [
 | 
				
			||||||
            appPackages
 | 
					          glibc
 | 
				
			||||||
            ++ [
 | 
					          bubblewrap
 | 
				
			||||||
              zstd
 | 
					          xdg-dbus-proxy
 | 
				
			||||||
              gnutar
 | 
					 | 
				
			||||||
              coreutils
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,7 @@
 | 
				
			|||||||
package system
 | 
					package system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
@ -43,13 +41,7 @@ func (a *ACL) apply(sys *I) error {
 | 
				
			|||||||
func (a *ACL) revert(sys *I, ec *Criteria) error {
 | 
					func (a *ACL) revert(sys *I, ec *Criteria) error {
 | 
				
			||||||
	if ec.hasType(a) {
 | 
						if ec.hasType(a) {
 | 
				
			||||||
		sys.println("stripping ACL", a)
 | 
							sys.println("stripping ACL", a)
 | 
				
			||||||
		err := acl.Update(a.path, sys.uid)
 | 
							return sys.wrapErrSuffix(acl.Update(a.path, sys.uid),
 | 
				
			||||||
		if errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			// the ACL is effectively stripped if the file no longer exists
 | 
					 | 
				
			||||||
			sys.printf("target of ACL %s no longer exists", a)
 | 
					 | 
				
			||||||
			err = nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return sys.wrapErrSuffix(err,
 | 
					 | 
				
			||||||
			fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
 | 
								fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		sys.println("skipping ACL", a)
 | 
							sys.println("skipping ACL", a)
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ package system
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@ -97,12 +96,7 @@ func (d *DBus) revert(sys *I, _ *Criteria) error {
 | 
				
			|||||||
	sys.println("terminating message bus proxy")
 | 
						sys.println("terminating message bus proxy")
 | 
				
			||||||
	d.proxy.Close()
 | 
						d.proxy.Close()
 | 
				
			||||||
	defer sys.println("message bus proxy exit")
 | 
						defer sys.println("message bus proxy exit")
 | 
				
			||||||
	err := d.proxy.Wait()
 | 
						return sys.wrapErrSuffix(d.proxy.Wait(), "message bus proxy error:")
 | 
				
			||||||
	if errors.Is(err, context.Canceled) {
 | 
					 | 
				
			||||||
		sys.println("message bus proxy canceled upstream")
 | 
					 | 
				
			||||||
		err = nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return sys.wrapErrSuffix(err, "message bus proxy error:")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *DBus) Is(o Op) bool {
 | 
					func (d *DBus) Is(o Op) bool {
 | 
				
			||||||
 | 
				
			|||||||
@ -102,21 +102,6 @@
 | 
				
			|||||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
					    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apps = [
 | 
					    apps = [
 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "check-sandbox";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        share = pkgs.foot;
 | 
					 | 
				
			||||||
        packages = [ ];
 | 
					 | 
				
			||||||
        command = "${pkgs.callPackage ./sandbox {
 | 
					 | 
				
			||||||
          inherit (config.environment.fortify.package) version;
 | 
					 | 
				
			||||||
        }}";
 | 
					 | 
				
			||||||
        extraPaths = [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            src = "/proc/mounts";
 | 
					 | 
				
			||||||
            dst = "/.fortify/host-mounts";
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name = "ne-foot";
 | 
					        name = "ne-foot";
 | 
				
			||||||
        verbose = true;
 | 
					        verbose = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,61 +0,0 @@
 | 
				
			|||||||
package sandbox
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	assert     = log.New(os.Stderr, "sandbox: ", 0)
 | 
					 | 
				
			||||||
	fatalfFunc = assert.Fatalf
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func MustAssertMounts(name, hostMountsFile, wantFile string) {
 | 
					 | 
				
			||||||
	hostMounts := make([]*Mntent, 0, 128)
 | 
					 | 
				
			||||||
	if err := IterMounts(hostMountsFile, func(e *Mntent) {
 | 
					 | 
				
			||||||
		hostMounts = append(hostMounts, e)
 | 
					 | 
				
			||||||
	}); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot parse host mounts: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var want []Mntent
 | 
					 | 
				
			||||||
	if f, err := os.Open(wantFile); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot open %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
	} else if err = json.NewDecoder(f).Decode(&want); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot decode %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
	} else if err = f.Close(); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot close %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for i := range want {
 | 
					 | 
				
			||||||
		if want[i].Opts == "host_passthrough" {
 | 
					 | 
				
			||||||
			for _, ent := range hostMounts {
 | 
					 | 
				
			||||||
				if want[i].FSName == ent.FSName {
 | 
					 | 
				
			||||||
					want[i].Opts = ent.Opts
 | 
					 | 
				
			||||||
					goto out
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			fatalf("host passthrough missing %q", want[i].FSName)
 | 
					 | 
				
			||||||
		out:
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	i := 0
 | 
					 | 
				
			||||||
	if err := IterMounts(name, func(e *Mntent) {
 | 
					 | 
				
			||||||
		if i == len(want) {
 | 
					 | 
				
			||||||
			fatalf("got more than %d entries", i)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if *e != want[i] {
 | 
					 | 
				
			||||||
			fatalf("entry %d\n got: %s\nwant: %s", i,
 | 
					 | 
				
			||||||
				e, &want[i])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Printf("%s", e)
 | 
					 | 
				
			||||||
		i++
 | 
					 | 
				
			||||||
	}); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot iterate mounts: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
package sandbox
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func ReplaceFatal(f func(format string, v ...any)) { fatalfFunc = f }
 | 
					 | 
				
			||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  writeShellScript,
 | 
					 | 
				
			||||||
  callPackage,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  version,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
writeShellScript "check-sandbox" ''
 | 
					 | 
				
			||||||
  set -e
 | 
					 | 
				
			||||||
  ${callPackage ./mount.nix { inherit version; }}/bin/test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  touch /tmp/sandbox-ok
 | 
					 | 
				
			||||||
''
 | 
					 | 
				
			||||||
@ -1,134 +0,0 @@
 | 
				
			|||||||
package sandbox
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
#include <stdlib.h>
 | 
					 | 
				
			||||||
#include <stdio.h>
 | 
					 | 
				
			||||||
#include <mntent.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const char *F_PROC_MOUNTS = "";
 | 
					 | 
				
			||||||
const char *F_SET_TYPE = "r";
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
import "C"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"runtime"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
	"unsafe"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Mntent struct {
 | 
					 | 
				
			||||||
	/* name of mounted filesystem */
 | 
					 | 
				
			||||||
	FSName string `json:"fsname"`
 | 
					 | 
				
			||||||
	/* filesystem path prefix */
 | 
					 | 
				
			||||||
	Dir string `json:"dir"`
 | 
					 | 
				
			||||||
	/* mount type (see mntent.h) */
 | 
					 | 
				
			||||||
	Type string `json:"type"`
 | 
					 | 
				
			||||||
	/* mount options (see mntent.h) */
 | 
					 | 
				
			||||||
	Opts string `json:"opts"`
 | 
					 | 
				
			||||||
	/* dump frequency in days */
 | 
					 | 
				
			||||||
	Freq int `json:"freq"`
 | 
					 | 
				
			||||||
	/* pass number on parallel fsck */
 | 
					 | 
				
			||||||
	Passno int `json:"passno"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *Mntent) String() string {
 | 
					 | 
				
			||||||
	return fmt.Sprintf("%s %s %s %s %d %d",
 | 
					 | 
				
			||||||
		e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func IterMounts(name string, f func(e *Mntent)) error {
 | 
					 | 
				
			||||||
	m := new(mounts)
 | 
					 | 
				
			||||||
	m.p = name
 | 
					 | 
				
			||||||
	if err := m.open(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for m.scan() {
 | 
					 | 
				
			||||||
		e := new(Mntent)
 | 
					 | 
				
			||||||
		m.copy(e)
 | 
					 | 
				
			||||||
		f(e)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	m.close()
 | 
					 | 
				
			||||||
	return m.Err()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type mounts struct {
 | 
					 | 
				
			||||||
	p  string
 | 
					 | 
				
			||||||
	f  *C.FILE
 | 
					 | 
				
			||||||
	mu sync.RWMutex
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ent *C.struct_mntent
 | 
					 | 
				
			||||||
	err error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *mounts) open() error {
 | 
					 | 
				
			||||||
	m.mu.Lock()
 | 
					 | 
				
			||||||
	defer m.mu.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.f != nil {
 | 
					 | 
				
			||||||
		panic("open called twice")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.p == "" {
 | 
					 | 
				
			||||||
		m.p = "/proc/mounts"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	name := C.CString(m.p)
 | 
					 | 
				
			||||||
	f, err := C.setmntent(name, C.F_SET_TYPE)
 | 
					 | 
				
			||||||
	C.free(unsafe.Pointer(name))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if f == nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	m.f = f
 | 
					 | 
				
			||||||
	runtime.SetFinalizer(m, (*mounts).close)
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *mounts) close() {
 | 
					 | 
				
			||||||
	m.mu.Lock()
 | 
					 | 
				
			||||||
	defer m.mu.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.f == nil {
 | 
					 | 
				
			||||||
		panic("close called before open")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	C.endmntent(m.f)
 | 
					 | 
				
			||||||
	runtime.SetFinalizer(m, nil)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *mounts) scan() bool {
 | 
					 | 
				
			||||||
	m.mu.Lock()
 | 
					 | 
				
			||||||
	defer m.mu.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.f == nil {
 | 
					 | 
				
			||||||
		panic("invalid file")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	m.ent, m.err = C.getmntent(m.f)
 | 
					 | 
				
			||||||
	return m.ent != nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *mounts) Err() error {
 | 
					 | 
				
			||||||
	m.mu.RLock()
 | 
					 | 
				
			||||||
	defer m.mu.RUnlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return m.err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *mounts) copy(v *Mntent) {
 | 
					 | 
				
			||||||
	m.mu.RLock()
 | 
					 | 
				
			||||||
	defer m.mu.RUnlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.ent == nil {
 | 
					 | 
				
			||||||
		panic("invalid entry")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	v.FSName = C.GoString(m.ent.mnt_fsname)
 | 
					 | 
				
			||||||
	v.Dir = C.GoString(m.ent.mnt_dir)
 | 
					 | 
				
			||||||
	v.Type = C.GoString(m.ent.mnt_type)
 | 
					 | 
				
			||||||
	v.Opts = C.GoString(m.ent.mnt_opts)
 | 
					 | 
				
			||||||
	v.Freq = int(m.ent.mnt_freq)
 | 
					 | 
				
			||||||
	v.Passno = int(m.ent.mnt_passno)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,79 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  writeText,
 | 
					 | 
				
			||||||
  buildGoModule,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  version,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
let
 | 
					 | 
				
			||||||
  wantMounts =
 | 
					 | 
				
			||||||
    let
 | 
					 | 
				
			||||||
      ent = fsname: dir: type: opts: freq: passno: {
 | 
					 | 
				
			||||||
        inherit
 | 
					 | 
				
			||||||
          fsname
 | 
					 | 
				
			||||||
          dir
 | 
					 | 
				
			||||||
          type
 | 
					 | 
				
			||||||
          opts
 | 
					 | 
				
			||||||
          freq
 | 
					 | 
				
			||||||
          passno
 | 
					 | 
				
			||||||
          ;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    in
 | 
					 | 
				
			||||||
    [
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/" "tmpfs" "rw,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "proc" "/proc" "proc" "rw,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/.fortify" "tmpfs" "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/dev" "tmpfs" "rw,nosuid,nodev,relatime,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/null" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/zero" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/full" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/random" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/urandom" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/tty" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devpts" "/dev/pts" "devpts" "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666" 0 0)
 | 
					 | 
				
			||||||
      (ent "mqueue" "/dev/mqueue" "mqueue" "rw,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/usr/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/nix/store" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/run/current-system" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/block" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/bus" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/class" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/dev" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/devices" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/run/opengl-driver" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/dri" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "proc" "/.fortify/host-mounts" "proc" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/.fortify/etc" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user" "tmpfs" "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user/65534" "tmpfs" "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/tmp" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/var/lib/fortify/u0/a1" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  mainFile = writeText "main.go" ''
 | 
					 | 
				
			||||||
    package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    import "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    func main() { sandbox.MustAssertMounts("", "/.fortify/host-mounts", "${writeText "want-mounts.json" (builtins.toJSON wantMounts)}") }
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
in
 | 
					 | 
				
			||||||
buildGoModule {
 | 
					 | 
				
			||||||
  pname = "check-mounts";
 | 
					 | 
				
			||||||
  inherit version;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  src = ../.;
 | 
					 | 
				
			||||||
  vendorHash = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  preBuild = ''
 | 
					 | 
				
			||||||
    go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
 | 
					 | 
				
			||||||
    cp ${mainFile} main.go
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,136 +0,0 @@
 | 
				
			|||||||
package sandbox_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/test/sandbox"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestMounts(t *testing.T) {
 | 
					 | 
				
			||||||
	testCases := []struct {
 | 
					 | 
				
			||||||
		name string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		sample string
 | 
					 | 
				
			||||||
		want   []sandbox.Mntent
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
 | 
					 | 
				
			||||||
mqueue /dev/mqueue mqueue rw,relatime 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
 | 
					 | 
				
			||||||
/dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0
 | 
					 | 
				
			||||||
overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on 0 0
 | 
					 | 
				
			||||||
`, []sandbox.Mntent{
 | 
					 | 
				
			||||||
			{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
 | 
					 | 
				
			||||||
			{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
 | 
					 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
					 | 
				
			||||||
			{"overlay", "/.fortify/sbin/fortify", "overlay", "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on", 0, 0},
 | 
					 | 
				
			||||||
		}},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, tc := range testCases {
 | 
					 | 
				
			||||||
		name := path.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) {
 | 
					 | 
				
			||||||
			i := 0
 | 
					 | 
				
			||||||
			if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
 | 
					 | 
				
			||||||
				if i == len(tc.want) {
 | 
					 | 
				
			||||||
					t.Errorf("IterMounts: got more than %d entries", i)
 | 
					 | 
				
			||||||
					t.FailNow()
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if *e != tc.want[i] {
 | 
					 | 
				
			||||||
					t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
 | 
					 | 
				
			||||||
						e, &tc.want[i])
 | 
					 | 
				
			||||||
					t.FailNow()
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				i++
 | 
					 | 
				
			||||||
			}); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("IterMounts: error = %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		t.Run(tc.name+" assert", func(t *testing.T) {
 | 
					 | 
				
			||||||
			sandbox.ReplaceFatal(t.Fatalf)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			wantFile := path.Join(t.TempDir(), "want.json")
 | 
					 | 
				
			||||||
			if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("cannot create %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
			} else if err = json.NewEncoder(f).Encode(tc.want); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("cannot encode to %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
			} else if err = f.Close(); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("cannot close %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			sandbox.MustAssertMounts(name, name, wantFile)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if err := os.Remove(wantFile); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("cannot remove %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := os.Remove(name); err != nil {
 | 
					 | 
				
			||||||
			t.Fatalf("cannot remove %q: %v", name, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										59
									
								
								test/test.py
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								test/test.py
									
									
									
									
									
								
							@ -95,19 +95,12 @@ print(denyOutput)
 | 
				
			|||||||
denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
 | 
					denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
 | 
				
			||||||
print(denyOutputVerbose)
 | 
					print(denyOutputVerbose)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Fail direct fsu call:
 | 
					 | 
				
			||||||
print(machine.fail("sudo -u alice -i fsu"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Verify PrintBaseError behaviour:
 | 
					# Verify PrintBaseError behaviour:
 | 
				
			||||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
					if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
				
			||||||
    raise Exception(f"unexpected deny output:\n{denyOutput}")
 | 
					    raise Exception(f"unexpected deny output:\n{denyOutput}")
 | 
				
			||||||
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
					if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
				
			||||||
    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
					    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check sandbox state:
 | 
					 | 
				
			||||||
swaymsg("exec check-sandbox")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start fortify permissive defaults outside Wayland session:
 | 
					# Start fortify permissive defaults outside Wayland session:
 | 
				
			||||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
					print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
				
			||||||
@ -117,55 +110,36 @@ output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
 | 
				
			|||||||
if output != "":
 | 
					if output != "":
 | 
				
			||||||
    raise Exception(f"unexpected output\n{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")
 | 
					 | 
				
			||||||
    # aid 0 does not have home-manager
 | 
					 | 
				
			||||||
    machine.send_chars(f"exec fortify run {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/fortify.1000/tmpdir/0/pd-silent-ready")
 | 
					 | 
				
			||||||
    machine.succeed("rm /tmp/fortify.1000/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 'fortify run {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:
 | 
					# Verify graceful failure on bad Wayland display name:
 | 
				
			||||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
 | 
					print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Start fortify permissive defaults within Wayland session:
 | 
					# Start fortify permissive defaults within Wayland session:
 | 
				
			||||||
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
					fortify(
 | 
				
			||||||
 | 
					    '-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
				
			||||||
machine.wait_for_file("/tmp/dbus-done")
 | 
					machine.wait_for_file("/tmp/dbus-done")
 | 
				
			||||||
collect_state_ui("dbus_notify_exited")
 | 
					collect_state_ui("dbus_notify_exited")
 | 
				
			||||||
machine.succeed("pkill -9 mako")
 | 
					machine.succeed("pkill -9 mako")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with Wayland enablement:
 | 
					# Start app (foot) with Wayland enablement:
 | 
				
			||||||
swaymsg("exec ne-foot")
 | 
					swaymsg("exec ne-foot")
 | 
				
			||||||
wait_for_window("u0_a2@machine")
 | 
					wait_for_window("u0_a1@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
 | 
				
			||||||
collect_state_ui("foot_wayland")
 | 
					collect_state_ui("foot_wayland")
 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					check_state("ne-foot", 1)
 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					# Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
 | 
					print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
 | 
					machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with Wayland enablement from a terminal:
 | 
					# Start app (foot) with Wayland enablement from a terminal:
 | 
				
			||||||
swaymsg(
 | 
					swaymsg(
 | 
				
			||||||
    "exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
					    "exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
				
			||||||
wait_for_window("u0_a2@machine")
 | 
					wait_for_window("u0_a1@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
 | 
				
			||||||
machine.wait_for_file("/tmp/ps-show-ok")
 | 
					machine.wait_for_file("/tmp/ps-show-ok")
 | 
				
			||||||
collect_state_ui("foot_wayland_term")
 | 
					collect_state_ui("foot_wayland_term")
 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					check_state("ne-foot", 1)
 | 
				
			||||||
@ -176,9 +150,9 @@ machine.wait_until_fails("pgrep foot")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Test PulseAudio (fortify does not support PipeWire yet):
 | 
					# Test PulseAudio (fortify does not support PipeWire yet):
 | 
				
			||||||
swaymsg("exec pa-foot")
 | 
					swaymsg("exec pa-foot")
 | 
				
			||||||
wait_for_window("u0_a3@machine")
 | 
					wait_for_window("u0_a2@machine")
 | 
				
			||||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
					machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
 | 
				
			||||||
collect_state_ui("pulse_wayland")
 | 
					collect_state_ui("pulse_wayland")
 | 
				
			||||||
check_state("pa-foot", 9)
 | 
					check_state("pa-foot", 9)
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
@ -186,9 +160,9 @@ machine.wait_until_fails("pgrep foot")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Test XWayland (foot does not support X):
 | 
					# Test XWayland (foot does not support X):
 | 
				
			||||||
swaymsg("exec x11-alacritty")
 | 
					swaymsg("exec x11-alacritty")
 | 
				
			||||||
wait_for_window("u0_a4@machine")
 | 
					wait_for_window("u0_a3@machine")
 | 
				
			||||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
					machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
 | 
				
			||||||
collect_state_ui("alacritty_x11")
 | 
					collect_state_ui("alacritty_x11")
 | 
				
			||||||
check_state("x11-alacritty", 2)
 | 
					check_state("x11-alacritty", 2)
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
@ -196,23 +170,24 @@ machine.wait_until_fails("pgrep alacritty")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with direct Wayland access:
 | 
					# Start app (foot) with direct Wayland access:
 | 
				
			||||||
swaymsg("exec da-foot")
 | 
					swaymsg("exec da-foot")
 | 
				
			||||||
wait_for_window("u0_a5@machine")
 | 
					wait_for_window("u0_a4@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct")
 | 
					machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-direct")
 | 
				
			||||||
collect_state_ui("foot_direct")
 | 
					collect_state_ui("foot_direct")
 | 
				
			||||||
check_state("da-foot", 1)
 | 
					check_state("da-foot", 1)
 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					# Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
 | 
					print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004"))
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005")
 | 
					machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test syscall filter:
 | 
					# Test syscall filter:
 | 
				
			||||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
					print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Exit Sway and verify process exit status 0:
 | 
					# Exit Sway and verify process exit status 0:
 | 
				
			||||||
swaymsg("exit", succeed=False)
 | 
					swaymsg("exit", succeed=False)
 | 
				
			||||||
 | 
					machine.wait_until_fails("pgrep -x sway")
 | 
				
			||||||
machine.wait_for_file("/tmp/sway-exit-ok")
 | 
					machine.wait_for_file("/tmp/sway-exit-ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Print fortify runDir contents:
 | 
					# Print fortify runDir contents:
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user