Compare commits
	
		
			No commits in common. "d8e9d71f87db912d73345fec2f9fcb61b25231cf" and "71135f339a4384ec67fa87ed6624a09168a7e52b" have entirely different histories.
		
	
	
		
			d8e9d71f87
			...
			71135f339a
		
	
		
@ -22,23 +22,6 @@ jobs:
 | 
			
		||||
          path: result/*
 | 
			
		||||
          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:
 | 
			
		||||
    name: Data race detector
 | 
			
		||||
    runs-on: nix
 | 
			
		||||
@ -60,7 +43,6 @@ jobs:
 | 
			
		||||
    name: Flake checks
 | 
			
		||||
    needs:
 | 
			
		||||
      - fortify
 | 
			
		||||
      - fpkg
 | 
			
		||||
      - race
 | 
			
		||||
    runs-on: nix
 | 
			
		||||
    steps:
 | 
			
		||||
 | 
			
		||||
@ -7,9 +7,8 @@
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  stdenv,
 | 
			
		||||
  closureInfo,
 | 
			
		||||
  writeScript,
 | 
			
		||||
  writeScriptBin,
 | 
			
		||||
  runtimeShell,
 | 
			
		||||
  writeText,
 | 
			
		||||
  symlinkJoin,
 | 
			
		||||
@ -17,15 +16,12 @@
 | 
			
		||||
  runCommand,
 | 
			
		||||
  fetchFromGitHub,
 | 
			
		||||
 | 
			
		||||
  zstd,
 | 
			
		||||
  nix,
 | 
			
		||||
  sqlite,
 | 
			
		||||
 | 
			
		||||
  name ? throw "name is required",
 | 
			
		||||
  version ? throw "version is required",
 | 
			
		||||
  pname ? "${name}-${version}",
 | 
			
		||||
  modules ? [ ],
 | 
			
		||||
  nixosModules ? [ ],
 | 
			
		||||
  script ? ''
 | 
			
		||||
    exec "$SHELL" "$@"
 | 
			
		||||
  '',
 | 
			
		||||
@ -77,8 +73,6 @@ let
 | 
			
		||||
        etc.nixpkgs.source = nixpkgs.outPath;
 | 
			
		||||
        systemPackages = [ pkgs.nix ];
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      imports = nixosModules;
 | 
			
		||||
    };
 | 
			
		||||
  nixos = nixpkgs.lib.nixosSystem {
 | 
			
		||||
    inherit system;
 | 
			
		||||
@ -171,7 +165,11 @@ let
 | 
			
		||||
          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;
 | 
			
		||||
    nix_gl = if gpu then nixGL else null;
 | 
			
		||||
@ -180,73 +178,26 @@ let
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
stdenv.mkDerivation {
 | 
			
		||||
  name = "${pname}.pkg";
 | 
			
		||||
  inherit version;
 | 
			
		||||
  __structuredAttrs = true;
 | 
			
		||||
writeScriptBin "build-fpkg-${pname}" ''
 | 
			
		||||
  #!${runtimeShell} -el
 | 
			
		||||
  OUT="$(mktemp -d)"
 | 
			
		||||
  TAR="$(mktemp -u)"
 | 
			
		||||
  set -x
 | 
			
		||||
 | 
			
		||||
  nativeBuildInputs = [
 | 
			
		||||
    zstd
 | 
			
		||||
    nix
 | 
			
		||||
    sqlite
 | 
			
		||||
  ];
 | 
			
		||||
  nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
 | 
			
		||||
  nix store --store "$OUT" optimise
 | 
			
		||||
  chmod -R +r "$OUT/nix/var"
 | 
			
		||||
  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 = ''
 | 
			
		||||
    NIX_ROOT="$(mktemp -d)"
 | 
			
		||||
    export USER="nobody"
 | 
			
		||||
  # creating an intermediate file improves zstd performance
 | 
			
		||||
  tar -C "$OUT" -cf "$TAR" .
 | 
			
		||||
  chmod +w -R "$OUT" && rm -rf "$OUT"
 | 
			
		||||
 | 
			
		||||
    # create bootstrap store
 | 
			
		||||
    bootstrapClosureInfo="${
 | 
			
		||||
      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"
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
  zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
 | 
			
		||||
  rm "$TAR"
 | 
			
		||||
''
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"log"
 | 
			
		||||
	"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"
 | 
			
		||||
	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/sys"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const shellPath = "/run/current-system/sw/bin/bash"
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	errSuccess = errors.New("success")
 | 
			
		||||
 | 
			
		||||
	std sys.State = new(sys.Std)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	fmsg.Prepare("fpkg")
 | 
			
		||||
	if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
			
		||||
		log.Fatalf("cannot set $SHELL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	// 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 (
 | 
			
		||||
var (
 | 
			
		||||
	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
 | 
			
		||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
			
		||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		var (
 | 
			
		||||
			flagDropShellActivate bool
 | 
			
		||||
		)
 | 
			
		||||
		c.NewCommand("install", "Install an application from its package", func(args []string) error {
 | 
			
		||||
			if len(args) != 1 {
 | 
			
		||||
				log.Println("invalid argument")
 | 
			
		||||
				return syscall.EINVAL
 | 
			
		||||
			}
 | 
			
		||||
			pkgPath := args[0]
 | 
			
		||||
			if !path.IsAbs(pkgPath) {
 | 
			
		||||
				if dir, err := os.Getwd(); err != nil {
 | 
			
		||||
					log.Printf("cannot get current directory: %v", err)
 | 
			
		||||
					return 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.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")
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	fmsg.Prepare("fpkg")
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	fmsg.Store(flagVerbose)
 | 
			
		||||
 | 
			
		||||
	args := flag.Args()
 | 
			
		||||
	if len(args) < 1 {
 | 
			
		||||
		log.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch args[0] {
 | 
			
		||||
	case "install":
 | 
			
		||||
		actionInstall(args[1:])
 | 
			
		||||
	case "start":
 | 
			
		||||
		actionStart(args[1:])
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		log.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	internal.Exit(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -70,32 +69,3 @@ func pathSetByApp(id string) *appPathSet {
 | 
			
		||||
	pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
			
		||||
	rs := new(fst.RunState)
 | 
			
		||||
	a := app.MustNew(std)
 | 
			
		||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
			
		||||
 | 
			
		||||
	if sa, err := a.Seal(config); err != nil {
 | 
			
		||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
			
		||||
		rs.ExitCode = 1
 | 
			
		||||
var (
 | 
			
		||||
	Fmain = compPoison
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
		// this updates ExitCode
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
			
		||||
		if fmsg.Load() {
 | 
			
		||||
			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()
 | 
			
		||||
		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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@ -11,11 +10,10 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func withNixDaemon(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
			
		||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
			
		||||
) {
 | 
			
		||||
	mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
			
		||||
	fortifyAppDropShell(updateConfig(&fst.Config{
 | 
			
		||||
		ID: app.ID,
 | 
			
		||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
			
		||||
			// start nix-daemon
 | 
			
		||||
@ -58,11 +56,8 @@ func withNixDaemon(
 | 
			
		||||
	}), dropShell, beforeFail)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func withCacheDir(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	action string, command []string, workDir string,
 | 
			
		||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
			
		||||
	mustRunAppDropShell(ctx, &fst.Config{
 | 
			
		||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
			
		||||
	fortifyAppDropShell(&fst.Config{
 | 
			
		||||
		ID:      app.ID,
 | 
			
		||||
		Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
			
		||||
		Confinement: fst.ConfinementConfig{
 | 
			
		||||
@ -95,12 +90,12 @@ func withCacheDir(
 | 
			
		||||
	}, 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 {
 | 
			
		||||
		config.Command = []string{shellPath, "-l"}
 | 
			
		||||
		mustRunApp(ctx, config, beforeFail)
 | 
			
		||||
		fortifyApp(config, beforeFail)
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		internal.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
	mustRunApp(ctx, config, beforeFail)
 | 
			
		||||
	fortifyApp(config, beforeFail)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	compPoison  = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
			
		||||
	fsuConfFile = "/etc/fsurc"
 | 
			
		||||
	envShim     = "FORTIFY_SHIM"
 | 
			
		||||
	envAID      = "FORTIFY_APP_ID"
 | 
			
		||||
@ -21,6 +22,10 @@ const (
 | 
			
		||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Fmain = compPoison
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	log.SetFlags(0)
 | 
			
		||||
	log.SetPrefix("fsu: ")
 | 
			
		||||
@ -35,16 +40,20 @@ func main() {
 | 
			
		||||
		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")
 | 
			
		||||
	if p, err := os.Readlink(pexe); err != nil {
 | 
			
		||||
		log.Fatalf("cannot read parent executable path: %v", err)
 | 
			
		||||
	} else if strings.HasSuffix(p, " (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")
 | 
			
		||||
	} else {
 | 
			
		||||
		toolPath = p
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// uid = 1000000 +
 | 
			
		||||
@ -138,9 +147,13 @@ func main() {
 | 
			
		||||
	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())
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	panic("unreachable")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkPath(p string) (string, bool) {
 | 
			
		||||
	return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  buildGoModule,
 | 
			
		||||
  fortify ? abort "fortify package required",
 | 
			
		||||
}:
 | 
			
		||||
@ -16,15 +15,5 @@ buildGoModule {
 | 
			
		||||
    go mod init fsu >& /dev/null
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  ldflags =
 | 
			
		||||
    lib.attrsets.foldlAttrs
 | 
			
		||||
      (
 | 
			
		||||
        ldflags: name: value:
 | 
			
		||||
        ldflags ++ [ "-X main.${name}=${value}" ]
 | 
			
		||||
      )
 | 
			
		||||
      [ "-s -w" ]
 | 
			
		||||
      {
 | 
			
		||||
        fmain = "${fortify}/libexec/fortify";
 | 
			
		||||
        fpkg = "${fortify}/libexec/fpkg";
 | 
			
		||||
      };
 | 
			
		||||
  ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h = helper.MustNewBwrap(bc, toolPath, true, p.seal, argF, nil, nil)
 | 
			
		||||
		h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
 | 
			
		||||
		p.bwrap = bc
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							@ -10,10 +10,9 @@ cp -rv "comp" "${out}"
 | 
			
		||||
 | 
			
		||||
go generate ./...
 | 
			
		||||
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.fsu=/usr/bin/fsu
 | 
			
		||||
  -X main.fmain=/usr/bin/fortify
 | 
			
		||||
  -X main.fpkg=/usr/bin/fpkg" ./...
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
 | 
			
		||||
  -X main.Fmain=/usr/bin/fortify" ./...
 | 
			
		||||
 | 
			
		||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
 | 
			
		||||
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
 | 
			
		||||
        {
 | 
			
		||||
          fortify = callPackage ./test { inherit system self; };
 | 
			
		||||
          fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
 | 
			
		||||
          race = callPackage ./test {
 | 
			
		||||
            inherit system self;
 | 
			
		||||
            withRace = true;
 | 
			
		||||
@ -68,7 +67,7 @@
 | 
			
		||||
            cd ${./.}
 | 
			
		||||
 | 
			
		||||
            echo "running nixfmt..."
 | 
			
		||||
            nixfmt --width=256 --check .
 | 
			
		||||
            nixfmt --check .
 | 
			
		||||
 | 
			
		||||
            touch $out
 | 
			
		||||
          '';
 | 
			
		||||
@ -98,32 +97,24 @@
 | 
			
		||||
      packages = forAllSystems (
 | 
			
		||||
        system:
 | 
			
		||||
        let
 | 
			
		||||
          inherit (self.packages.${system}) fortify fsu;
 | 
			
		||||
          inherit (self.packages.${system}) fortify;
 | 
			
		||||
          pkgs = nixpkgsFor.${system};
 | 
			
		||||
        in
 | 
			
		||||
        {
 | 
			
		||||
          default = fortify;
 | 
			
		||||
          default = self.packages.${system}.fortify;
 | 
			
		||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
			
		||||
            inherit (pkgs)
 | 
			
		||||
              bubblewrap
 | 
			
		||||
              xdg-dbus-proxy
 | 
			
		||||
              glibc
 | 
			
		||||
              zstd
 | 
			
		||||
              gnutar
 | 
			
		||||
              coreutils
 | 
			
		||||
              ;
 | 
			
		||||
            inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
			
		||||
          };
 | 
			
		||||
          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
 | 
			
		||||
                export XDG_CACHE_HOME="$(mktemp -d)"
 | 
			
		||||
 | 
			
		||||
                # get a different workdir as go does not like /build
 | 
			
		||||
            cd $(mktemp -d) \
 | 
			
		||||
                && cp -r ${fortify.src}/. . \
 | 
			
		||||
                && chmod +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
 | 
			
		||||
                && chmod -R +w .
 | 
			
		||||
                cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
 | 
			
		||||
 | 
			
		||||
                export FORTIFY_VERSION="v${fortify.version}"
 | 
			
		||||
                ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
			
		||||
@ -24,9 +23,6 @@ type bubblewrap struct {
 | 
			
		||||
	// name of the command to run in bwrap
 | 
			
		||||
	name string
 | 
			
		||||
 | 
			
		||||
	// whether to set process group id
 | 
			
		||||
	setpgid bool
 | 
			
		||||
 | 
			
		||||
	lock sync.RWMutex
 | 
			
		||||
	*helperCmd
 | 
			
		||||
}
 | 
			
		||||
@ -42,10 +38,6 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
 | 
			
		||||
	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.
 | 
			
		||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
			
		||||
func MustNewBwrap(
 | 
			
		||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
			
		||||
	conf *bwrap.Config, name string,
 | 
			
		||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
			
		||||
	extraFiles []*os.File,
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
) Helper {
 | 
			
		||||
	b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
 | 
			
		||||
	b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
@ -73,7 +65,7 @@ func MustNewBwrap(
 | 
			
		||||
// 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.
 | 
			
		||||
func NewBwrap(
 | 
			
		||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
			
		||||
	conf *bwrap.Config, name string,
 | 
			
		||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
			
		||||
	extraFiles []*os.File,
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
@ -81,7 +73,6 @@ func NewBwrap(
 | 
			
		||||
	b := new(bubblewrap)
 | 
			
		||||
 | 
			
		||||
	b.name = name
 | 
			
		||||
	b.setpgid = setpgid
 | 
			
		||||
	b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
 | 
			
		||||
 | 
			
		||||
	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(
 | 
			
		||||
			sc, "fortify", false,
 | 
			
		||||
			sc, "fortify",
 | 
			
		||||
			argsWt, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
@ -44,7 +44,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
			
		||||
		if got := helper.MustNewBwrap(
 | 
			
		||||
			sc, "fortify", false,
 | 
			
		||||
			sc, "fortify",
 | 
			
		||||
			argsWt, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		); got == nil {
 | 
			
		||||
@ -64,7 +64,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		helper.MustNewBwrap(
 | 
			
		||||
			&bwrap.Config{Hostname: "\x00"}, "fortify", false,
 | 
			
		||||
			&bwrap.Config{Hostname: "\x00"}, "fortify",
 | 
			
		||||
			nil, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
@ -74,7 +74,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
		helper.InternalReplaceExecCommand(t)
 | 
			
		||||
 | 
			
		||||
		h := helper.MustNewBwrap(
 | 
			
		||||
			sc, "crash-test-dummy", false,
 | 
			
		||||
			sc, "crash-test-dummy",
 | 
			
		||||
			nil, argFChecked,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
@ -98,11 +98,6 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
			
		||||
		testHelper(t, func() helper.Helper {
 | 
			
		||||
			return helper.MustNewBwrap(
 | 
			
		||||
				sc, "crash-test-dummy", false,
 | 
			
		||||
				argsWt, argF, nil, nil,
 | 
			
		||||
			)
 | 
			
		||||
		})
 | 
			
		||||
		testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", 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/helper"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
	*/
 | 
			
		||||
@ -84,8 +79,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
			
		||||
				ec.Set(system.Process)
 | 
			
		||||
				if states, err := c.Load(); err != nil {
 | 
			
		||||
					// revert per-process state here to limit damage
 | 
			
		||||
					storeErr.OpErr = err
 | 
			
		||||
					return seal.sys.Revert(ec)
 | 
			
		||||
					return errors.Join(err, seal.sys.Revert(ec))
 | 
			
		||||
				} else {
 | 
			
		||||
					if l := len(states); l == 0 {
 | 
			
		||||
						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:")
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
@ -172,9 +170,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
			
		||||
		Time: *rs.Time,
 | 
			
		||||
	}
 | 
			
		||||
	var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
 | 
			
		||||
	earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
 | 
			
		||||
		earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
 | 
			
		||||
	})
 | 
			
		||||
	earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) })
 | 
			
		||||
	// destroy defunct state entry
 | 
			
		||||
	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:")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
	}
 | 
			
		||||
	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) },
 | 
			
		||||
		extraFiles,
 | 
			
		||||
		syncFd,
 | 
			
		||||
 | 
			
		||||
@ -52,8 +52,14 @@ func (s *Shim) Start(
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
) (*time.Time, error) {
 | 
			
		||||
	// prepare user switcher invocation
 | 
			
		||||
	fsuPath := internal.MustFsuPath()
 | 
			
		||||
	s.cmd = exec.Command(fsuPath)
 | 
			
		||||
	var fsu string
 | 
			
		||||
	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
 | 
			
		||||
	if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,10 @@ package internal
 | 
			
		||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	version = compPoison
 | 
			
		||||
	Version = compPoison
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// check validates string value set at compile time.
 | 
			
		||||
func check(s string) (string, bool) { return s, s != compPoison && s != "" }
 | 
			
		||||
 | 
			
		||||
func Version() string {
 | 
			
		||||
	if v, ok := check(version); ok {
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
	return "impure"
 | 
			
		||||
// Check validates string value set at compile time.
 | 
			
		||||
func Check(s string) (string, bool) {
 | 
			
		||||
	return s, s != compPoison && s != ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,11 @@
 | 
			
		||||
package internal
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
import "path"
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	fsu = compPoison
 | 
			
		||||
	Fsu = compPoison
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func MustFsuPath() string {
 | 
			
		||||
	if name, ok := checkPath(fsu); ok {
 | 
			
		||||
		return name
 | 
			
		||||
	}
 | 
			
		||||
	fmsg.BeforeExit()
 | 
			
		||||
	log.Fatal("invalid fsu path, this program is compiled incorrectly")
 | 
			
		||||
	return compPoison
 | 
			
		||||
func Path(p string) (string, bool) {
 | 
			
		||||
	return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"os/user"
 | 
			
		||||
@ -78,10 +79,14 @@ func (s *Std) Uid(aid int) (int, error) {
 | 
			
		||||
	defer func() { s.uidCopy[aid] = u }()
 | 
			
		||||
 | 
			
		||||
	u.uid = -1
 | 
			
		||||
	fsuPath := internal.MustFsuPath()
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command(fsuPath)
 | 
			
		||||
	cmd.Path = fsuPath
 | 
			
		||||
	if fsu, ok := internal.Check(internal.Fsu); !ok {
 | 
			
		||||
		fmsg.BeforeExit()
 | 
			
		||||
		log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
 | 
			
		||||
		// unreachable
 | 
			
		||||
		return 0, syscall.EBADE
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd := exec.Command(fsu)
 | 
			
		||||
		cmd.Path = fsu
 | 
			
		||||
		cmd.Stderr = os.Stderr // pass through fatal messages
 | 
			
		||||
		cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
 | 
			
		||||
		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 {
 | 
			
		||||
			u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
 | 
			
		||||
		} 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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
 | 
			
		||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
			
		||||
			NewSession:    true,
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
		}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
 | 
			
		||||
		}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
 | 
			
		||||
		nil, func(_, _ int) []string { return []string{p} },
 | 
			
		||||
		nil, nil,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								main.go
									
									
									
									
									
								
							@ -68,20 +68,10 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
		flagVerbose bool
 | 
			
		||||
		flagJSON    bool
 | 
			
		||||
	)
 | 
			
		||||
	c := command.New(out, log.Printf, "fortify", func([]string) error {
 | 
			
		||||
		fmsg.Store(flagVerbose)
 | 
			
		||||
		if flagVerbose {
 | 
			
		||||
			seccomp.CPrintln = log.Println
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}).
 | 
			
		||||
	c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
 | 
			
		||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
			
		||||
		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 {
 | 
			
		||||
		if len(args) < 1 {
 | 
			
		||||
			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")
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@ -278,21 +272,45 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runApp(a fst.App, config *fst.Config) {
 | 
			
		||||
	rs := new(fst.RunState)
 | 
			
		||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
			
		||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
	defer stop() // unreachable
 | 
			
		||||
 | 
			
		||||
	rs := new(fst.RunState)
 | 
			
		||||
	if fmsg.Load() {
 | 
			
		||||
		seccomp.CPrintln = log.Println
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sa, err := a.Seal(config); err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
		// this updates ExitCode
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
			
		||||
			logWaitError(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								nixos.nix
									
									
									
									
									
								
							@ -77,12 +77,21 @@ 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;
 | 
			
		||||
                    };
 | 
			
		||||
                  command = if app.command == null then app.name else app.command;
 | 
			
		||||
                  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 = {
 | 
			
		||||
                    inherit (app) id;
 | 
			
		||||
                    command = [
 | 
			
		||||
@ -156,7 +165,9 @@ in
 | 
			
		||||
                  };
 | 
			
		||||
                in
 | 
			
		||||
                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;
 | 
			
		||||
            in
 | 
			
		||||
 | 
			
		||||
@ -3,14 +3,7 @@
 | 
			
		||||
let
 | 
			
		||||
  inherit (lib) types mkOption mkEnableOption;
 | 
			
		||||
  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
			
		||||
    inherit (pkgs)
 | 
			
		||||
      bubblewrap
 | 
			
		||||
      xdg-dbus-proxy
 | 
			
		||||
      glibc
 | 
			
		||||
      zstd
 | 
			
		||||
      gnutar
 | 
			
		||||
      coreutils
 | 
			
		||||
      ;
 | 
			
		||||
    inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.nix
									
									
									
									
									
								
							@ -14,11 +14,6 @@
 | 
			
		||||
  wayland-scanner,
 | 
			
		||||
  xorg,
 | 
			
		||||
 | 
			
		||||
  # for fpkg
 | 
			
		||||
  zstd,
 | 
			
		||||
  gnutar,
 | 
			
		||||
  coreutils,
 | 
			
		||||
 | 
			
		||||
  glibc, # for ldd
 | 
			
		||||
  withStatic ? stdenv.hostPlatform.isStatic,
 | 
			
		||||
}:
 | 
			
		||||
@ -30,7 +25,10 @@ buildGoModule rec {
 | 
			
		||||
  src = builtins.path {
 | 
			
		||||
    name = "${pname}-src";
 | 
			
		||||
    path = lib.cleanSource ./.;
 | 
			
		||||
    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
			
		||||
    filter =
 | 
			
		||||
      path: type:
 | 
			
		||||
      !(type == "regular" && lib.hasSuffix ".nix" path)
 | 
			
		||||
      && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
			
		||||
  };
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
@ -41,15 +39,17 @@ buildGoModule rec {
 | 
			
		||||
        ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
 | 
			
		||||
      )
 | 
			
		||||
      (
 | 
			
		||||
        [ "-s -w" ]
 | 
			
		||||
        [
 | 
			
		||||
          "-s -w"
 | 
			
		||||
        ]
 | 
			
		||||
        ++ lib.optionals withStatic [
 | 
			
		||||
          "-linkmode external"
 | 
			
		||||
          "-extldflags \"-static\""
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
      {
 | 
			
		||||
        version = "v${version}";
 | 
			
		||||
        fsu = "/run/wrappers/bin/fsu";
 | 
			
		||||
        Version = "v${version}";
 | 
			
		||||
        Fsu = "/run/wrappers/bin/fsu";
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  # nix build environment does not allow acls
 | 
			
		||||
@ -79,33 +79,19 @@ buildGoModule rec {
 | 
			
		||||
    HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  postInstall =
 | 
			
		||||
    let
 | 
			
		||||
      appPackages = [
 | 
			
		||||
        glibc
 | 
			
		||||
        bubblewrap
 | 
			
		||||
        xdg-dbus-proxy
 | 
			
		||||
      ];
 | 
			
		||||
    in
 | 
			
		||||
    ''
 | 
			
		||||
  postInstall = ''
 | 
			
		||||
    install -D --target-directory=$out/share/zsh/site-functions comp/*
 | 
			
		||||
 | 
			
		||||
    mkdir "$out/libexec"
 | 
			
		||||
    mv "$out"/bin/* "$out/libexec/"
 | 
			
		||||
 | 
			
		||||
    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 : ${
 | 
			
		||||
          lib.makeBinPath (
 | 
			
		||||
            appPackages
 | 
			
		||||
            ++ [
 | 
			
		||||
              zstd
 | 
			
		||||
              gnutar
 | 
			
		||||
              coreutils
 | 
			
		||||
        lib.makeBinPath [
 | 
			
		||||
          glibc
 | 
			
		||||
          bubblewrap
 | 
			
		||||
          xdg-dbus-proxy
 | 
			
		||||
        ]
 | 
			
		||||
          )
 | 
			
		||||
      }
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,7 @@
 | 
			
		||||
package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"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 {
 | 
			
		||||
	if ec.hasType(a) {
 | 
			
		||||
		sys.println("stripping ACL", a)
 | 
			
		||||
		err := 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,
 | 
			
		||||
		return sys.wrapErrSuffix(acl.Update(a.path, sys.uid),
 | 
			
		||||
			fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
 | 
			
		||||
	} else {
 | 
			
		||||
		sys.println("skipping ACL", a)
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"log"
 | 
			
		||||
	"strings"
 | 
			
		||||
@ -97,12 +96,7 @@ func (d *DBus) revert(sys *I, _ *Criteria) error {
 | 
			
		||||
	sys.println("terminating message bus proxy")
 | 
			
		||||
	d.proxy.Close()
 | 
			
		||||
	defer sys.println("message bus proxy exit")
 | 
			
		||||
	err := d.proxy.Wait()
 | 
			
		||||
	if errors.Is(err, context.Canceled) {
 | 
			
		||||
		sys.println("message bus proxy canceled upstream")
 | 
			
		||||
		err = nil
 | 
			
		||||
	}
 | 
			
		||||
	return sys.wrapErrSuffix(err, "message bus proxy error:")
 | 
			
		||||
	return sys.wrapErrSuffix(d.proxy.Wait(), "message bus proxy error:")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DBus) Is(o Op) bool {
 | 
			
		||||
 | 
			
		||||
@ -102,21 +102,6 @@
 | 
			
		||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
			
		||||
 | 
			
		||||
    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";
 | 
			
		||||
        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")
 | 
			
		||||
print(denyOutputVerbose)
 | 
			
		||||
 | 
			
		||||
# Fail direct fsu call:
 | 
			
		||||
print(machine.fail("sudo -u alice -i fsu"))
 | 
			
		||||
 | 
			
		||||
# Verify PrintBaseError behaviour:
 | 
			
		||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
			
		||||
    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":
 | 
			
		||||
    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:
 | 
			
		||||
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")
 | 
			
		||||
@ -117,55 +110,36 @@ output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
 | 
			
		||||
if output != "":
 | 
			
		||||
    raise Exception(f"unexpected output\n{output}")
 | 
			
		||||
 | 
			
		||||
# Verify silent output permissive defaults signal:
 | 
			
		||||
def silent_output_interrupt(flags):
 | 
			
		||||
    swaymsg("exec foot")
 | 
			
		||||
    wait_for_window("alice@machine")
 | 
			
		||||
    # 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:
 | 
			
		||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
 | 
			
		||||
 | 
			
		||||
# 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")
 | 
			
		||||
collect_state_ui("dbus_notify_exited")
 | 
			
		||||
machine.succeed("pkill -9 mako")
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with Wayland enablement:
 | 
			
		||||
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.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")
 | 
			
		||||
check_state("ne-foot", 1)
 | 
			
		||||
# 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.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")
 | 
			
		||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with Wayland enablement from a terminal:
 | 
			
		||||
swaymsg(
 | 
			
		||||
    "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.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")
 | 
			
		||||
collect_state_ui("foot_wayland_term")
 | 
			
		||||
check_state("ne-foot", 1)
 | 
			
		||||
@ -176,9 +150,9 @@ machine.wait_until_fails("pgrep foot")
 | 
			
		||||
 | 
			
		||||
# Test PulseAudio (fortify does not support PipeWire yet):
 | 
			
		||||
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.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")
 | 
			
		||||
check_state("pa-foot", 9)
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
@ -186,9 +160,9 @@ machine.wait_until_fails("pgrep foot")
 | 
			
		||||
 | 
			
		||||
# Test XWayland (foot does not support X):
 | 
			
		||||
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.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")
 | 
			
		||||
check_state("x11-alacritty", 2)
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
@ -196,23 +170,24 @@ machine.wait_until_fails("pgrep alacritty")
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with direct Wayland access:
 | 
			
		||||
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.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")
 | 
			
		||||
check_state("da-foot", 1)
 | 
			
		||||
# 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.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 1000005")
 | 
			
		||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004")
 | 
			
		||||
 | 
			
		||||
# Test syscall filter:
 | 
			
		||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
			
		||||
 | 
			
		||||
# Exit Sway and verify process exit status 0:
 | 
			
		||||
swaymsg("exit", succeed=False)
 | 
			
		||||
machine.wait_until_fails("pgrep -x sway")
 | 
			
		||||
machine.wait_for_file("/tmp/sway-exit-ok")
 | 
			
		||||
 | 
			
		||||
# Print fortify runDir contents:
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user