Compare commits
	
		
			144 Commits
		
	
	
		
			71135f339a
			...
			371dd5b938
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 371dd5b938 | |||
| 4836d570ae | |||
| 985f9442e6 | |||
| 67eb28466d | |||
| c326c3f97d | |||
| 971c79bb80 | |||
| f86d868274 | |||
| 33940265a6 | |||
| b39f3aeb59 | |||
| 61dbfeffe7 | |||
| 532feb4bfa | |||
| ec5e91b8c9 | |||
| ee51320abf | |||
| 5c4058d5ac | |||
| e732dca762 | |||
| a9adcd914b | |||
| 3dd4ff29c8 | |||
| 61d86c5e10 | |||
| d097eaa28f | |||
| ad3576c164 | |||
| b989a4601a | |||
| a11237b158 | |||
| 40f00d570e | |||
| 0eb1bc6301 | |||
| 1eb837eab8 | |||
| 0a4e633db2 | |||
| e8809125d4 | |||
| 806ce18c0a | |||
| b71d2bf534 | |||
| 46059b1840 | |||
| d2c329bcea | |||
| 2d379b5a38 | |||
| 75e0c5d406 | |||
| 770b37ae16 | |||
| c638193268 | |||
| 8c3a817881 | |||
| e2fce321c1 | |||
| 241702ae3a | |||
| d21d9c5b1d | |||
| a70daf2250 | |||
| 632b18addd | |||
| a57a7a6a16 | |||
| 5098b12e4a | |||
| 9ddf5794dd | |||
| b74a08dda9 | |||
| 1b9408864f | |||
| cc89dbdf63 | |||
| 228f3301f2 | |||
| 07181138e5 | |||
| 816b372f14 | |||
| d7eddd54a2 | |||
| 7c063833e0 | |||
| af3619d440 | |||
| 528674cb6e | |||
| 70c9757e26 | |||
| c83a7e2efc | |||
| 904208b87f | |||
| 007b52d81f | |||
| 3385538142 | |||
| 24618ab9a1 | |||
| 9ce4706a07 | |||
| 9a1f8e129f | |||
| ee10860357 | |||
| 44277dc0f1 | |||
| bc54db54d2 | |||
| bf07b7cd9e | |||
| 5d3c8dcc92 | |||
| 48feca800f | |||
| 42de09e896 | |||
| 1576fea8a3 | |||
| ae522ab364 | |||
| 273d97af85 | |||
| 891316d924 | |||
| 9f5dad1998 | |||
| 6e7ddb2d2e | |||
| bac4e67867 | |||
| 4230281194 | |||
| e64e7608ca | |||
| 10a21ce3ef | |||
| 0f1f0e4364 | |||
| f9bf20a3c7 | |||
| 73c1a83032 | |||
| f443d315ad | |||
| 9e18d1de77 | |||
| 2647a71be1 | |||
| 7c60a4d8e8 | |||
| 4bb5d9780f | |||
| f41fd94628 | |||
| 94895bbacb | |||
| f332200ca4 | |||
| 2eff470091 | |||
| a092b042ab | |||
| e94b09d337 | |||
| 5d9e669d97 | |||
| f1002157a5 | |||
| 4133b555ba | |||
| 9b1a60b5c9 | |||
| beb3918809 | |||
| 2871426df2 | |||
| e048f31baa | |||
| 6af8b8859f | |||
| f38ba7e923 | |||
| d22145a392 | |||
| 29c3f8becb | |||
| be16970e77 | |||
| df266527f1 | |||
| c8ed7aae6e | |||
| 61e58aa14d | |||
| 9e15898c8f | |||
| f7bd6a5a41 | |||
| ea853e21d9 | |||
| 0bd9b9e8fe | |||
| 39e32799b3 | |||
| 9953768de5 | |||
| 0d3652b793 | |||
| d8e9d71f87 | |||
| 558974b996 | |||
| 4de4049713 | |||
| 2d4cabe786 | |||
| 80f9b62d25 | |||
| 673b648bd3 | |||
| 45ad788c6d | |||
| 56539d8db5 | |||
| 840ceb615a | |||
| 741d011543 | |||
| d050b3de25 | |||
| 5de28800ad | |||
| 8e50293ab7 | |||
| 12c6d66bfd | |||
| d7d2bd33ed | |||
| c21a4cff14 | |||
| 4fa38d6063 | |||
| 6d4ac3d9fd | |||
| a5d2f040fb | |||
| c62689e17f | |||
| 39dc8e7bd8 | |||
| 5a732d153e | |||
| b4549c72be | |||
| 1818dc3a4c | |||
| 65094b63cd | |||
| f0a082ec84 | |||
| 751aa350ee | |||
| e6cd2bb2a8 | |||
| 0fb72e5d99 | 
@ -22,6 +22,23 @@ jobs:
 | 
				
			|||||||
          path: result/*
 | 
					          path: result/*
 | 
				
			||||||
          retention-days: 1
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fpkg:
 | 
				
			||||||
 | 
					    name: Fpkg
 | 
				
			||||||
 | 
					    runs-on: nix
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Checkout
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Run NixOS test
 | 
				
			||||||
 | 
					        run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.fpkg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Upload test output
 | 
				
			||||||
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: "fpkg-vm-output"
 | 
				
			||||||
 | 
					          path: result/*
 | 
				
			||||||
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  race:
 | 
					  race:
 | 
				
			||||||
    name: Data race detector
 | 
					    name: Data race detector
 | 
				
			||||||
    runs-on: nix
 | 
					    runs-on: nix
 | 
				
			||||||
@ -43,6 +60,7 @@ jobs:
 | 
				
			|||||||
    name: Flake checks
 | 
					    name: Flake checks
 | 
				
			||||||
    needs:
 | 
					    needs:
 | 
				
			||||||
      - fortify
 | 
					      - fortify
 | 
				
			||||||
 | 
					      - fpkg
 | 
				
			||||||
      - race
 | 
					      - race
 | 
				
			||||||
    runs-on: nix
 | 
					    runs-on: nix
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
				
			|||||||
@ -4,12 +4,15 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type bundleInfo struct {
 | 
					type appInfo struct {
 | 
				
			||||||
	Name    string `json:"name"`
 | 
						Name    string `json:"name"`
 | 
				
			||||||
	Version string `json:"version"`
 | 
						Version string `json:"version"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,13 +23,15 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Groups []string `json:"groups,omitempty"`
 | 
						Groups []string `json:"groups,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
						Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
 | 
						Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
						Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
						Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
						Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
						MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
@ -36,13 +41,11 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
						SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablement `json:"enablements"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through inverted to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Devel bool `json:"devel,omitempty"`
 | 
					 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
					 | 
				
			||||||
	Multiarch bool `json:"multiarch,omitempty"`
 | 
						Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Bluetooth bool `json:"bluetooth,omitempty"`
 | 
						Bluetooth bool `json:"bluetooth,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// allow gpu access within sandbox
 | 
						// allow gpu access within sandbox
 | 
				
			||||||
@ -59,8 +62,64 @@ type bundleInfo struct {
 | 
				
			|||||||
	ActivationPackage string `json:"activation_package"`
 | 
						ActivationPackage string `json:"activation_package"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
 | 
					func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
 | 
				
			||||||
	bundle := new(bundleInfo)
 | 
						config := &fst.Config{
 | 
				
			||||||
 | 
							ID:   app.ID,
 | 
				
			||||||
 | 
							Path: argv[0],
 | 
				
			||||||
 | 
							Args: 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),
 | 
				
			||||||
 | 
									Devel:         app.Devel,
 | 
				
			||||||
 | 
									Userns:        app.Userns,
 | 
				
			||||||
 | 
									Net:           app.Net,
 | 
				
			||||||
 | 
									Dev:           app.Dev,
 | 
				
			||||||
 | 
									Tty:           app.Tty || 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,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Multiarch {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Bluetooth {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadAppInfo(name string, beforeFail func()) *appInfo {
 | 
				
			||||||
 | 
						bundle := new(appInfo)
 | 
				
			||||||
	if f, err := os.Open(name); err != nil {
 | 
						if f, err := os.Open(name); err != nil {
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		log.Fatalf("cannot open bundle: %v", err)
 | 
							log.Fatalf("cannot open bundle: %v", err)
 | 
				
			||||||
@ -7,8 +7,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
 | 
					  stdenv,
 | 
				
			||||||
 | 
					  closureInfo,
 | 
				
			||||||
  writeScript,
 | 
					  writeScript,
 | 
				
			||||||
  writeScriptBin,
 | 
					 | 
				
			||||||
  runtimeShell,
 | 
					  runtimeShell,
 | 
				
			||||||
  writeText,
 | 
					  writeText,
 | 
				
			||||||
  symlinkJoin,
 | 
					  symlinkJoin,
 | 
				
			||||||
@ -16,12 +17,15 @@
 | 
				
			|||||||
  runCommand,
 | 
					  runCommand,
 | 
				
			||||||
  fetchFromGitHub,
 | 
					  fetchFromGitHub,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  zstd,
 | 
				
			||||||
  nix,
 | 
					  nix,
 | 
				
			||||||
 | 
					  sqlite,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  name ? throw "name is required",
 | 
					  name ? throw "name is required",
 | 
				
			||||||
  version ? throw "version is required",
 | 
					  version ? throw "version is required",
 | 
				
			||||||
  pname ? "${name}-${version}",
 | 
					  pname ? "${name}-${version}",
 | 
				
			||||||
  modules ? [ ],
 | 
					  modules ? [ ],
 | 
				
			||||||
 | 
					  nixosModules ? [ ],
 | 
				
			||||||
  script ? ''
 | 
					  script ? ''
 | 
				
			||||||
    exec "$SHELL" "$@"
 | 
					    exec "$SHELL" "$@"
 | 
				
			||||||
  '',
 | 
					  '',
 | 
				
			||||||
@ -73,6 +77,8 @@ let
 | 
				
			|||||||
        etc.nixpkgs.source = nixpkgs.outPath;
 | 
					        etc.nixpkgs.source = nixpkgs.outPath;
 | 
				
			||||||
        systemPackages = [ pkgs.nix ];
 | 
					        systemPackages = [ pkgs.nix ];
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      imports = nixosModules;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  nixos = nixpkgs.lib.nixosSystem {
 | 
					  nixos = nixpkgs.lib.nixosSystem {
 | 
				
			||||||
    inherit system;
 | 
					    inherit system;
 | 
				
			||||||
@ -165,11 +171,7 @@ let
 | 
				
			|||||||
          broadcast = { };
 | 
					          broadcast = { };
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    enablements =
 | 
					    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);
 | 
				
			||||||
      (if allow_wayland then 1 else 0)
 | 
					 | 
				
			||||||
      + (if allow_x11 then 2 else 0)
 | 
					 | 
				
			||||||
      + (if allow_dbus then 4 else 0)
 | 
					 | 
				
			||||||
      + (if allow_pulse then 8 else 0);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mesa = if gpu then mesaWrappers else null;
 | 
					    mesa = if gpu then mesaWrappers else null;
 | 
				
			||||||
    nix_gl = if gpu then nixGL else null;
 | 
					    nix_gl = if gpu then nixGL else null;
 | 
				
			||||||
@ -178,26 +180,73 @@ let
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
writeScriptBin "build-fpkg-${pname}" ''
 | 
					stdenv.mkDerivation {
 | 
				
			||||||
  #!${runtimeShell} -el
 | 
					  name = "${pname}.pkg";
 | 
				
			||||||
  OUT="$(mktemp -d)"
 | 
					  inherit version;
 | 
				
			||||||
  TAR="$(mktemp -u)"
 | 
					  __structuredAttrs = true;
 | 
				
			||||||
  set -x
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
 | 
					  nativeBuildInputs = [
 | 
				
			||||||
  nix store --store "$OUT" optimise
 | 
					    zstd
 | 
				
			||||||
  chmod -R +r "$OUT/nix/var"
 | 
					    nix
 | 
				
			||||||
  nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
 | 
					    sqlite
 | 
				
			||||||
    "${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"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # creating an intermediate file improves zstd performance
 | 
					  buildCommand = ''
 | 
				
			||||||
  tar -C "$OUT" -cf "$TAR" .
 | 
					    NIX_ROOT="$(mktemp -d)"
 | 
				
			||||||
  chmod +w -R "$OUT" && rm -rf "$OUT"
 | 
					    export USER="nobody"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
 | 
					    # create bootstrap store
 | 
				
			||||||
  rm "$TAR"
 | 
					    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"
 | 
				
			||||||
 | 
					  '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,191 +0,0 @@
 | 
				
			|||||||
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()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										351
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							
							
						
						
									
										351
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							@ -1,50 +1,351 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"flag"
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/command"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const shellPath = "/run/current-system/sw/bin/bash"
 | 
					const shellPath = "/run/current-system/sw/bin/bash"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						errSuccess = errors.New("success")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						std sys.State = new(sys.Std)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
 | 
						fmsg.Prepare("fpkg")
 | 
				
			||||||
	if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
						if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot set $SHELL: %v", err)
 | 
							log.Fatalf("cannot set $SHELL: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	flagVerbose bool
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	fmsg.Prepare("fpkg")
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.Parse()
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
	fmsg.Store(flagVerbose)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
 | 
							// not fatal: this program runs as the privileged user
 | 
				
			||||||
	args := flag.Args()
 | 
					 | 
				
			||||||
	if len(args) < 1 {
 | 
					 | 
				
			||||||
		log.Fatal("invalid argument")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch args[0] {
 | 
						if os.Geteuid() == 0 {
 | 
				
			||||||
	case "install":
 | 
							log.Fatal("this program must not run as root")
 | 
				
			||||||
		actionInstall(args[1:])
 | 
					 | 
				
			||||||
	case "start":
 | 
					 | 
				
			||||||
		actionStart(args[1:])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		log.Fatal("invalid argument")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal.Exit(0)
 | 
						ctx, stop := signal.NotifyContext(context.Background(),
 | 
				
			||||||
 | 
							syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							flagVerbose   bool
 | 
				
			||||||
 | 
							flagDropShell bool
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
 | 
				
			||||||
 | 
							internal.InstallFmsg(flagVerbose)
 | 
				
			||||||
 | 
							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")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); 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 := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
				
			||||||
 | 
								pathSet := pathSetByApp(bundle.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								a := 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 {
 | 
				
			||||||
 | 
									a = loadAppInfo(pathSet.metaPath, cleanup)
 | 
				
			||||||
 | 
									if a.ID != bundle.ID {
 | 
				
			||||||
 | 
										cleanup()
 | 
				
			||||||
 | 
										log.Printf("app %q claims to have identifier %q",
 | 
				
			||||||
 | 
											bundle.ID, a.ID)
 | 
				
			||||||
 | 
										return syscall.EBADE
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// sec: should verify credentials
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if a != bundle {
 | 
				
			||||||
 | 
									// do not try to re-install
 | 
				
			||||||
 | 
									if a.NixGL == bundle.NixGL &&
 | 
				
			||||||
 | 
										a.CurrentSystem == bundle.CurrentSystem &&
 | 
				
			||||||
 | 
										a.Launcher == bundle.Launcher &&
 | 
				
			||||||
 | 
										a.ActivationPackage == bundle.ActivationPackage {
 | 
				
			||||||
 | 
										cleanup()
 | 
				
			||||||
 | 
										log.Printf("package %q is identical to local application %q",
 | 
				
			||||||
 | 
											pkgPath, a.ID)
 | 
				
			||||||
 | 
										return errSuccess
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// AppID determines uid
 | 
				
			||||||
 | 
									if a.AppID != bundle.AppID {
 | 
				
			||||||
 | 
										cleanup()
 | 
				
			||||||
 | 
										log.Printf("package %q app id %d differs from installed %d",
 | 
				
			||||||
 | 
											pkgPath, bundle.AppID, a.AppID)
 | 
				
			||||||
 | 
										return syscall.EBADE
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// sec: should compare version string
 | 
				
			||||||
 | 
									fmsg.Verbosef("installing application %q version %q over local %q",
 | 
				
			||||||
 | 
										bundle.ID, bundle.Version, a.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)
 | 
				
			||||||
 | 
								a := loadAppInfo(pathSet.metaPath, func() {})
 | 
				
			||||||
 | 
								if a.ID != id {
 | 
				
			||||||
 | 
									log.Printf("app %q claims to have identifier %q", id, a.ID)
 | 
				
			||||||
 | 
									return syscall.EBADE
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/*
 | 
				
			||||||
 | 
									Prepare nixGL.
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if a.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:" + a.NixGL,
 | 
				
			||||||
 | 
										"nix build --impure " +
 | 
				
			||||||
 | 
											"--out-link /nix/.nixGL/auto/vulkan " +
 | 
				
			||||||
 | 
											"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
				
			||||||
 | 
											"path:" + a.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
 | 
				
			||||||
 | 
									}, a, pathSet, flagDropShellNixGL, func() {})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/*
 | 
				
			||||||
 | 
									Create app configuration.
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								argv := make([]string, 1, len(args))
 | 
				
			||||||
 | 
								if !flagDropShell {
 | 
				
			||||||
 | 
									argv[0] = a.Launcher
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									argv[0] = shellPath
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								argv = append(argv, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								config := a.toFst(pathSet, argv, flagDropShell)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/*
 | 
				
			||||||
 | 
									Expose GPU devices.
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if a.GPU {
 | 
				
			||||||
 | 
									config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
 | 
				
			||||||
 | 
										&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
 | 
				
			||||||
 | 
									appendGPUFilesystem(config)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/*
 | 
				
			||||||
 | 
									Spawn app.
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								mustRunApp(ctx, config, func() {})
 | 
				
			||||||
 | 
								return errSuccess
 | 
				
			||||||
 | 
							}).
 | 
				
			||||||
 | 
								Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
 | 
				
			||||||
 | 
								Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.MustParse(os.Args[1:], func(err error) {
 | 
				
			||||||
 | 
							fmsg.Verbosef("command returned %v", err)
 | 
				
			||||||
 | 
							if errors.Is(err, errSuccess) {
 | 
				
			||||||
 | 
								fmsg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(0)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						log.Fatal("unreachable")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -69,3 +70,32 @@ func pathSetByApp(id string) *appPathSet {
 | 
				
			|||||||
	pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
						pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
				
			||||||
	return pathSet
 | 
						return pathSet
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func appendGPUFilesystem(config *fst.Config) {
 | 
				
			||||||
 | 
						config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
							// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
 | 
				
			||||||
 | 
							{Src: "/dev/dri", Device: true},
 | 
				
			||||||
 | 
							// mali
 | 
				
			||||||
 | 
							{Src: "/dev/mali", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/mali0", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/umplock", Device: true},
 | 
				
			||||||
 | 
							// nvidia
 | 
				
			||||||
 | 
							{Src: "/dev/nvidiactl", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-modeset", Device: true},
 | 
				
			||||||
 | 
							// nvidia OpenCL/CUDA
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-uvm", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia-uvm-tools", Device: true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
 | 
				
			||||||
 | 
							{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
 | 
				
			||||||
 | 
						}...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,65 +1,28 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
					func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
				
			||||||
 | 
						rs := new(fst.RunState)
 | 
				
			||||||
 | 
						a := app.MustNew(ctx, std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
						if sa, err := a.Seal(config); err != nil {
 | 
				
			||||||
	Fmain = compPoison
 | 
							fmsg.PrintBaseError(err, "cannot seal app:")
 | 
				
			||||||
)
 | 
							rs.ExitCode = 1
 | 
				
			||||||
 | 
					 | 
				
			||||||
func fortifyApp(config *fst.Config, beforeFail func()) {
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		cmd *exec.Cmd
 | 
					 | 
				
			||||||
		st  io.WriteCloser
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if p, ok := internal.Path(Fmain); !ok {
 | 
					 | 
				
			||||||
		beforeFail()
 | 
					 | 
				
			||||||
		log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
 | 
					 | 
				
			||||||
	} else if r, w, err := os.Pipe(); err != nil {
 | 
					 | 
				
			||||||
		beforeFail()
 | 
					 | 
				
			||||||
		log.Fatalf("cannot pipe: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if fmsg.Load() {
 | 
							// this updates ExitCode
 | 
				
			||||||
			cmd = exec.Command(p, "-v", "app", "3")
 | 
							app.PrintRunStateErr(rs, sa.Run(rs))
 | 
				
			||||||
		} 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
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						if rs.ExitCode != 0 {
 | 
				
			||||||
		if err := json.NewEncoder(st).Encode(config); err != nil {
 | 
					 | 
				
			||||||
			beforeFail()
 | 
					 | 
				
			||||||
			log.Fatalf("cannot send configuration: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := cmd.Start(); err != nil {
 | 
					 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		log.Fatalf("cannot start fortify: %v", err)
 | 
							os.Exit(rs.ExitCode)
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,178 +0,0 @@
 | 
				
			|||||||
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},
 | 
					 | 
				
			||||||
	}...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										60
									
								
								cmd/fpkg/test/configuration.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								cmd/fpkg/test/configuration.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					{ 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=session 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"; };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								cmd/fpkg/test/default.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								cmd/fpkg/test/default.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								cmd/fpkg/test/foot.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								cmd/fpkg/test/foot.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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 "$@"
 | 
				
			||||||
 | 
					  '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										108
									
								
								cmd/fpkg/test/test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								cmd/fpkg/test/test.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					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['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
 | 
				
			||||||
 | 
					        raise Exception(f"unexpected args {instance['config']['args']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,21 +1,24 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withNixDaemon(
 | 
					func withNixDaemon(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
						action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
				
			||||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
						app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	fortifyAppDropShell(updateConfig(&fst.Config{
 | 
						mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
				
			||||||
		ID: app.ID,
 | 
							ID:   app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
							Path: shellPath,
 | 
				
			||||||
 | 
							Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
				
			||||||
			// start nix-daemon
 | 
								// start nix-daemon
 | 
				
			||||||
			"nix-daemon --store / & " +
 | 
								"nix-daemon --store / & " +
 | 
				
			||||||
			// wait for socket to appear
 | 
								// wait for socket to appear
 | 
				
			||||||
@ -32,11 +35,11 @@ func withNixDaemon(
 | 
				
			|||||||
			Inner:    path.Join("/data/data", app.ID),
 | 
								Inner:    path.Join("/data/data", app.ID),
 | 
				
			||||||
			Outer:    pathSet.homeDir,
 | 
								Outer:    pathSet.homeDir,
 | 
				
			||||||
			Sandbox: &fst.SandboxConfig{
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname: formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
				UserNS:       true, // nix sandbox requires userns
 | 
									Userns:   true, // nix sandbox requires userns
 | 
				
			||||||
				Net:          net,
 | 
									Net:      net,
 | 
				
			||||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
									Seccomp:  seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									Tty:      dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
										{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
@ -56,19 +59,23 @@ func withNixDaemon(
 | 
				
			|||||||
	}), dropShell, beforeFail)
 | 
						}), dropShell, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
					func withCacheDir(
 | 
				
			||||||
	fortifyAppDropShell(&fst.Config{
 | 
						ctx context.Context,
 | 
				
			||||||
		ID:      app.ID,
 | 
						action string, command []string, workDir string,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
						app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
				
			||||||
 | 
						mustRunAppDropShell(ctx, &fst.Config{
 | 
				
			||||||
 | 
							ID:   app.ID,
 | 
				
			||||||
 | 
							Path: shellPath,
 | 
				
			||||||
 | 
							Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
				
			||||||
		Confinement: fst.ConfinementConfig{
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
			AppID:    app.AppID,
 | 
								AppID:    app.AppID,
 | 
				
			||||||
			Username: "nixos",
 | 
								Username: "nixos",
 | 
				
			||||||
			Inner:    path.Join("/data/data", app.ID, "cache"),
 | 
								Inner:    path.Join("/data/data", app.ID, "cache"),
 | 
				
			||||||
			Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
								Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
				
			||||||
			Sandbox: &fst.SandboxConfig{
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname: formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
									Seccomp:  seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									Tty:      dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
										{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
				
			||||||
					{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
										{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
				
			||||||
@ -90,12 +97,12 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
 | 
				
			|||||||
	}, dropShell, beforeFail)
 | 
						}, dropShell, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
 | 
					func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	if dropShell {
 | 
						if dropShell {
 | 
				
			||||||
		config.Command = []string{shellPath, "-l"}
 | 
							config.Args = []string{shellPath, "-l"}
 | 
				
			||||||
		fortifyApp(config, beforeFail)
 | 
							mustRunApp(ctx, config, beforeFail)
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		internal.Exit(0)
 | 
							internal.Exit(0)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fortifyApp(config, beforeFail)
 | 
						mustRunApp(ctx, config, beforeFail)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,6 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	compPoison  = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
					 | 
				
			||||||
	fsuConfFile = "/etc/fsurc"
 | 
						fsuConfFile = "/etc/fsurc"
 | 
				
			||||||
	envShim     = "FORTIFY_SHIM"
 | 
						envShim     = "FORTIFY_SHIM"
 | 
				
			||||||
	envAID      = "FORTIFY_APP_ID"
 | 
						envAID      = "FORTIFY_APP_ID"
 | 
				
			||||||
@ -22,10 +21,6 @@ const (
 | 
				
			|||||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
						PR_SET_NO_NEW_PRIVS = 0x26
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	Fmain = compPoison
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	log.SetFlags(0)
 | 
						log.SetFlags(0)
 | 
				
			||||||
	log.SetPrefix("fsu: ")
 | 
						log.SetPrefix("fsu: ")
 | 
				
			||||||
@ -40,20 +35,16 @@ func main() {
 | 
				
			|||||||
		log.Fatal("this program must not be started by root")
 | 
							log.Fatal("this program must not be started by root")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var fmain string
 | 
						var toolPath string
 | 
				
			||||||
	if p, ok := checkPath(Fmain); !ok {
 | 
					 | 
				
			||||||
		log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmain = p
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
 | 
						pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
 | 
				
			||||||
	if p, err := os.Readlink(pexe); err != nil {
 | 
						if p, err := os.Readlink(pexe); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot read parent executable path: %v", err)
 | 
							log.Fatalf("cannot read parent executable path: %v", err)
 | 
				
			||||||
	} else if strings.HasSuffix(p, " (deleted)") {
 | 
						} else if strings.HasSuffix(p, " (deleted)") {
 | 
				
			||||||
		log.Fatal("fortify executable has been deleted")
 | 
							log.Fatal("fortify executable has been deleted")
 | 
				
			||||||
	} else if p != fmain {
 | 
						} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
 | 
				
			||||||
		log.Fatal("this program must be started by fortify")
 | 
							log.Fatal("this program must be started by fortify")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							toolPath = p
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// uid = 1000000 +
 | 
						// uid = 1000000 +
 | 
				
			||||||
@ -147,13 +138,9 @@ func main() {
 | 
				
			|||||||
	if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
 | 
						if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
 | 
				
			||||||
		log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
 | 
							log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
 | 
						if err := syscall.Exec(toolPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot start shim: %v", err)
 | 
							log.Fatalf("cannot start shim: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	panic("unreachable")
 | 
						panic("unreachable")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func checkPath(p string) (string, bool) {
 | 
					 | 
				
			||||||
	return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
 | 
					  lib,
 | 
				
			||||||
  buildGoModule,
 | 
					  buildGoModule,
 | 
				
			||||||
  fortify ? abort "fortify package required",
 | 
					  fortify ? abort "fortify package required",
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
@ -15,5 +16,15 @@ buildGoModule {
 | 
				
			|||||||
    go mod init fsu >& /dev/null
 | 
					    go mod init fsu >& /dev/null
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
 | 
					  ldflags =
 | 
				
			||||||
 | 
					    lib.attrsets.foldlAttrs
 | 
				
			||||||
 | 
					      (
 | 
				
			||||||
 | 
					        ldflags: name: value:
 | 
				
			||||||
 | 
					        ldflags ++ [ "-X main.${name}=${value}" ]
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      [ "-s -w" ]
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        fmain = "${fortify}/libexec/fortify";
 | 
				
			||||||
 | 
					        fpkg = "${fortify}/libexec/fpkg";
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								cmd/fsu/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								cmd/fsu/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,14 +1,21 @@
 | 
				
			|||||||
package dbus_test
 | 
					package dbus_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestNew(t *testing.T) {
 | 
					func TestNew(t *testing.T) {
 | 
				
			||||||
@ -100,15 +107,20 @@ func TestProxy_Seal(t *testing.T) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
 | 
					func TestProxy_Start_Wait_Close_String(t *testing.T) {
 | 
				
			||||||
	t.Run("sandboxed", func(t *testing.T) {
 | 
						oldWaitDelay := helper.WaitDelay
 | 
				
			||||||
 | 
						helper.WaitDelay = 16 * time.Second
 | 
				
			||||||
 | 
						t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("sandbox", func(t *testing.T) {
 | 
				
			||||||
 | 
							proxyName := dbus.ProxyName
 | 
				
			||||||
 | 
							dbus.ProxyName = os.Args[0]
 | 
				
			||||||
 | 
							t.Cleanup(func() { dbus.ProxyName = proxyName })
 | 
				
			||||||
		testProxyStartWaitCloseString(t, true)
 | 
							testProxyStartWaitCloseString(t, true)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	t.Run("direct", func(t *testing.T) {
 | 
						t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
 | 
				
			||||||
		testProxyStartWaitCloseString(t, false)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
					func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
 | 
				
			||||||
	for id, tc := range testCasePairs() {
 | 
						for id, tc := range testCasePairs() {
 | 
				
			||||||
		// this test does not test errors
 | 
							// this test does not test errors
 | 
				
			||||||
		if tc[0].wantErr {
 | 
							if tc[0].wantErr {
 | 
				
			||||||
@ -125,14 +137,33 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("proxy for "+id, func(t *testing.T) {
 | 
							t.Run("proxy for "+id, func(t *testing.T) {
 | 
				
			||||||
			helper.InternalReplaceExecCommand(t)
 | 
					 | 
				
			||||||
			overridePath(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			p := dbus.New(tc[0].bus, tc[1].bus)
 | 
								p := dbus.New(tc[0].bus, tc[1].bus)
 | 
				
			||||||
 | 
								p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
 | 
				
			||||||
 | 
									return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
										"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.CmdF = func(v any) {
 | 
				
			||||||
 | 
									if useSandbox {
 | 
				
			||||||
 | 
										container := v.(*sandbox.Container)
 | 
				
			||||||
 | 
										if container.Args[0] != dbus.ProxyName {
 | 
				
			||||||
 | 
											panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										cmd := v.(*exec.Cmd)
 | 
				
			||||||
 | 
										if cmd.Args[0] != dbus.ProxyName {
 | 
				
			||||||
 | 
											panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										cmd.Err = nil
 | 
				
			||||||
 | 
										cmd.Path = os.Args[0]
 | 
				
			||||||
 | 
										cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
 | 
				
			||||||
			output := new(strings.Builder)
 | 
								output := new(strings.Builder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("unsealed behaviour of "+id, func(t *testing.T) {
 | 
								t.Run("unsealed", func(t *testing.T) {
 | 
				
			||||||
				t.Run("unsealed string of "+id, func(t *testing.T) {
 | 
									t.Run("string", func(t *testing.T) {
 | 
				
			||||||
					want := "(unsealed dbus proxy)"
 | 
										want := "(unsealed dbus proxy)"
 | 
				
			||||||
					if got := p.String(); got != want {
 | 
										if got := p.String(); got != want {
 | 
				
			||||||
						t.Errorf("String() = %v, want %v",
 | 
											t.Errorf("String() = %v, want %v",
 | 
				
			||||||
@ -141,16 +172,16 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("unsealed start of "+id, func(t *testing.T) {
 | 
									t.Run("start", func(t *testing.T) {
 | 
				
			||||||
					want := "proxy not sealed"
 | 
										want := "proxy not sealed"
 | 
				
			||||||
					if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
 | 
										if err := p.Start(context.Background(), nil, useSandbox); err == nil || err.Error() != want {
 | 
				
			||||||
						t.Errorf("Start() error = %v, wantErr %q",
 | 
											t.Errorf("Start() error = %v, wantErr %q",
 | 
				
			||||||
							err, errors.New(want))
 | 
												err, errors.New(want))
 | 
				
			||||||
						return
 | 
											return
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("unsealed wait of "+id, func(t *testing.T) {
 | 
									t.Run("wait", func(t *testing.T) {
 | 
				
			||||||
					wantErr := "dbus: not started"
 | 
										wantErr := "dbus: not started"
 | 
				
			||||||
					if err := p.Wait(); err == nil || err.Error() != wantErr {
 | 
										if err := p.Wait(); err == nil || err.Error() != wantErr {
 | 
				
			||||||
						t.Errorf("Wait() error = %v, wantErr %v",
 | 
											t.Errorf("Wait() error = %v, wantErr %v",
 | 
				
			||||||
@ -168,7 +199,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("sealed behaviour of "+id, func(t *testing.T) {
 | 
								t.Run("sealed", func(t *testing.T) {
 | 
				
			||||||
				want := strings.Join(append(tc[0].want, tc[1].want...), " ")
 | 
									want := strings.Join(append(tc[0].want, tc[1].want...), " ")
 | 
				
			||||||
				if got := p.String(); got != want {
 | 
									if got := p.String(); got != want {
 | 
				
			||||||
					t.Errorf("String() = %v, want %v",
 | 
										t.Errorf("String() = %v, want %v",
 | 
				
			||||||
@ -176,17 +207,20 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
					return
 | 
										return
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("sealed start of "+id, func(t *testing.T) {
 | 
									t.Run("start", func(t *testing.T) {
 | 
				
			||||||
					ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
										ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
					defer cancel()
 | 
										defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					if err := p.Start(ctx, output, sandbox); err != nil {
 | 
										if err := p.Start(ctx, output, useSandbox); err != nil {
 | 
				
			||||||
						t.Fatalf("Start(nil, nil) error = %v",
 | 
											t.Fatalf("Start(nil, nil) error = %v",
 | 
				
			||||||
							err)
 | 
												err)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					t.Run("started string of "+id, func(t *testing.T) {
 | 
										t.Run("string", func(t *testing.T) {
 | 
				
			||||||
						wantSubstr := dbus.ProxyName + " --args="
 | 
											wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
 | 
				
			||||||
 | 
											if useSandbox {
 | 
				
			||||||
 | 
												wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x3e`, os.Args[0])
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
						if got := p.String(); !strings.Contains(got, wantSubstr) {
 | 
											if got := p.String(); !strings.Contains(got, wantSubstr) {
 | 
				
			||||||
							t.Errorf("String() = %v, want %v",
 | 
												t.Errorf("String() = %v, want %v",
 | 
				
			||||||
								p.String(), wantSubstr)
 | 
													p.String(), wantSubstr)
 | 
				
			||||||
@ -194,7 +228,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
						}
 | 
											}
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					t.Run("started wait of "+id, func(t *testing.T) {
 | 
										t.Run("wait", func(t *testing.T) {
 | 
				
			||||||
						p.Close()
 | 
											p.Close()
 | 
				
			||||||
						if err := p.Wait(); err != nil {
 | 
											if err := p.Wait(); err != nil {
 | 
				
			||||||
							t.Errorf("Wait() error = %v\noutput: %s",
 | 
												t.Errorf("Wait() error = %v\noutput: %s",
 | 
				
			||||||
@ -207,10 +241,10 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func overridePath(t *testing.T) {
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
	proxyName := dbus.ProxyName
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
	dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
 | 
							return
 | 
				
			||||||
	t.Cleanup(func() {
 | 
						}
 | 
				
			||||||
		dbus.ProxyName = proxyName
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
	})
 | 
						sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										178
									
								
								dbus/proc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								dbus/proc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					package dbus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Start launches the D-Bus proxy.
 | 
				
			||||||
 | 
					func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) error {
 | 
				
			||||||
 | 
						p.lock.Lock()
 | 
				
			||||||
 | 
						defer p.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.seal == nil {
 | 
				
			||||||
 | 
							return errors.New("proxy not sealed")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var h helper.Helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, cancel := context.WithCancelCause(ctx)
 | 
				
			||||||
 | 
						if !useSandbox {
 | 
				
			||||||
 | 
							h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
 | 
				
			||||||
 | 
								if p.CmdF != nil {
 | 
				
			||||||
 | 
									p.CmdF(cmd)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if output != nil {
 | 
				
			||||||
 | 
									cmd.Stdout, cmd.Stderr = output, output
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
				
			||||||
 | 
								cmd.Env = make([]string, 0)
 | 
				
			||||||
 | 
							}, nil)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							toolPath := p.name
 | 
				
			||||||
 | 
							if filepath.Base(p.name) == p.name {
 | 
				
			||||||
 | 
								if s, err := exec.LookPath(p.name); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									toolPath = s
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var libPaths []string
 | 
				
			||||||
 | 
							if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								libPaths = ldd.Path(entries)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h = helper.New(
 | 
				
			||||||
 | 
								c, toolPath,
 | 
				
			||||||
 | 
								p.seal, true,
 | 
				
			||||||
 | 
								argF, func(container *sandbox.Container) {
 | 
				
			||||||
 | 
									container.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
									container.Hostname = "fortify-dbus"
 | 
				
			||||||
 | 
									container.CommandContext = p.CommandContext
 | 
				
			||||||
 | 
									if output != nil {
 | 
				
			||||||
 | 
										container.Stdout, container.Stderr = output, output
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if p.CmdF != nil {
 | 
				
			||||||
 | 
										p.CmdF(container)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// these lib paths are unpredictable, so mount them first so they cannot cover anything
 | 
				
			||||||
 | 
									for _, name := range libPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, 0)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// upstream bus directories
 | 
				
			||||||
 | 
									upstreamPaths := make([]string, 0, 2)
 | 
				
			||||||
 | 
									for _, as := range []string{p.session[0], p.system[0]} {
 | 
				
			||||||
 | 
										if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
 | 
				
			||||||
 | 
											// leave / intact
 | 
				
			||||||
 | 
											upstreamPaths = append(upstreamPaths, path.Dir(as[10:]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									slices.Sort(upstreamPaths)
 | 
				
			||||||
 | 
									upstreamPaths = slices.Compact(upstreamPaths)
 | 
				
			||||||
 | 
									for _, name := range upstreamPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, 0)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// parent directories of bind paths
 | 
				
			||||||
 | 
									sockDirPaths := make([]string, 0, 2)
 | 
				
			||||||
 | 
									if d := path.Dir(p.session[1]); path.IsAbs(d) {
 | 
				
			||||||
 | 
										sockDirPaths = append(sockDirPaths, d)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if d := path.Dir(p.system[1]); path.IsAbs(d) {
 | 
				
			||||||
 | 
										sockDirPaths = append(sockDirPaths, d)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									slices.Sort(sockDirPaths)
 | 
				
			||||||
 | 
									sockDirPaths = slices.Compact(sockDirPaths)
 | 
				
			||||||
 | 
									for _, name := range sockDirPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, sandbox.BindWritable)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// xdg-dbus-proxy bin path
 | 
				
			||||||
 | 
									binPath := path.Dir(toolPath)
 | 
				
			||||||
 | 
									container.Bind(binPath, binPath, 0)
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := h.Start(); err != nil {
 | 
				
			||||||
 | 
							cancel(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p.helper = h
 | 
				
			||||||
 | 
						p.ctx = c
 | 
				
			||||||
 | 
						p.cancel = cancel
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var proxyClosed = errors.New("proxy closed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Wait blocks until xdg-dbus-proxy exits and releases resources.
 | 
				
			||||||
 | 
					func (p *Proxy) Wait() error {
 | 
				
			||||||
 | 
						p.lock.RLock()
 | 
				
			||||||
 | 
						defer p.lock.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.helper == nil {
 | 
				
			||||||
 | 
							return errors.New("dbus: not started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errs := make([]error, 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errs[0] = p.helper.Wait()
 | 
				
			||||||
 | 
						if p.cancel == nil &&
 | 
				
			||||||
 | 
							errors.Is(errs[0], context.Canceled) &&
 | 
				
			||||||
 | 
							errors.Is(context.Cause(p.ctx), proxyClosed) {
 | 
				
			||||||
 | 
							errs[0] = nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ensure socket removal so ephemeral directory is empty at revert
 | 
				
			||||||
 | 
						if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
							errs[1] = err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.sysP {
 | 
				
			||||||
 | 
							if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
								errs[2] = err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return errors.Join(errs...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
 | 
				
			||||||
 | 
					func (p *Proxy) Close() {
 | 
				
			||||||
 | 
						p.lock.Lock()
 | 
				
			||||||
 | 
						defer p.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.cancel == nil {
 | 
				
			||||||
 | 
							panic("dbus: not started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cancel(proxyClosed)
 | 
				
			||||||
 | 
						p.cancel = nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func argF(argsFd, statFd int) []string {
 | 
				
			||||||
 | 
						if statFd == -1 {
 | 
				
			||||||
 | 
							return []string{"--args=" + strconv.Itoa(argsFd)}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -5,10 +5,10 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ProxyName is the file name or path to the proxy program.
 | 
					// ProxyName is the file name or path to the proxy program.
 | 
				
			||||||
@ -19,15 +19,18 @@ var ProxyName = "xdg-dbus-proxy"
 | 
				
			|||||||
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
 | 
					// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
 | 
				
			||||||
type Proxy struct {
 | 
					type Proxy struct {
 | 
				
			||||||
	helper helper.Helper
 | 
						helper helper.Helper
 | 
				
			||||||
	bwrap  *bwrap.Config
 | 
					 | 
				
			||||||
	ctx    context.Context
 | 
						ctx    context.Context
 | 
				
			||||||
	cancel context.CancelCauseFunc
 | 
						cancel context.CancelCauseFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	name    string
 | 
						name    string
 | 
				
			||||||
	session [2]string
 | 
						session [2]string
 | 
				
			||||||
	system  [2]string
 | 
						system  [2]string
 | 
				
			||||||
 | 
						CmdF    func(any)
 | 
				
			||||||
	sysP    bool
 | 
						sysP    bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
				
			||||||
 | 
						FilterF        func([]byte) []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	seal io.WriterTo
 | 
						seal io.WriterTo
 | 
				
			||||||
	lock sync.RWMutex
 | 
						lock sync.RWMutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										175
									
								
								dbus/run.go
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								dbus/run.go
									
									
									
									
									
								
							@ -1,175 +0,0 @@
 | 
				
			|||||||
package dbus
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Start launches the D-Bus proxy.
 | 
					 | 
				
			||||||
func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
 | 
					 | 
				
			||||||
	p.lock.Lock()
 | 
					 | 
				
			||||||
	defer p.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.seal == nil {
 | 
					 | 
				
			||||||
		return errors.New("proxy not sealed")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		h helper.Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		argF = func(argsFD, statFD int) []string {
 | 
					 | 
				
			||||||
			if statFD == -1 {
 | 
					 | 
				
			||||||
				return []string{"--args=" + strconv.Itoa(argsFD)}
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				return []string{"--args=" + strconv.Itoa(argsFD), "--fd=" + strconv.Itoa(statFD)}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !sandbox {
 | 
					 | 
				
			||||||
		h = helper.New(p.seal, p.name, argF)
 | 
					 | 
				
			||||||
		// xdg-dbus-proxy does not need to inherit the environment
 | 
					 | 
				
			||||||
		h.SetEnv(make([]string, 0))
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// look up absolute path if name is just a file name
 | 
					 | 
				
			||||||
		toolPath := p.name
 | 
					 | 
				
			||||||
		if filepath.Base(p.name) == p.name {
 | 
					 | 
				
			||||||
			if s, err := exec.LookPath(p.name); err != nil {
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				toolPath = s
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve libraries by parsing ldd output
 | 
					 | 
				
			||||||
		var proxyDeps []*ldd.Entry
 | 
					 | 
				
			||||||
		if toolPath != "/nonexistent-xdg-dbus-proxy" {
 | 
					 | 
				
			||||||
			if l, err := ldd.Exec(ctx, toolPath); err != nil {
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				proxyDeps = l
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		bc := &bwrap.Config{
 | 
					 | 
				
			||||||
			Unshare:       nil,
 | 
					 | 
				
			||||||
			Hostname:      "fortify-dbus",
 | 
					 | 
				
			||||||
			Chdir:         "/",
 | 
					 | 
				
			||||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
					 | 
				
			||||||
			Clearenv:      true,
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve proxy socket directories
 | 
					 | 
				
			||||||
		bindTarget := make(map[string]struct{}, 2)
 | 
					 | 
				
			||||||
		for _, ps := range []string{p.session[1], p.system[1]} {
 | 
					 | 
				
			||||||
			if pd := path.Dir(ps); len(pd) > 0 {
 | 
					 | 
				
			||||||
				if pd[0] == '/' {
 | 
					 | 
				
			||||||
					bindTarget[pd] = struct{}{}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		for k := range bindTarget {
 | 
					 | 
				
			||||||
			bc.Bind(k, k, false, true)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// xdb-dbus-proxy bin and dependencies
 | 
					 | 
				
			||||||
		roBindTarget[path.Dir(toolPath)] = struct{}{}
 | 
					 | 
				
			||||||
		for _, ent := range proxyDeps {
 | 
					 | 
				
			||||||
			if path.IsAbs(ent.Path) {
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(ent.Path)] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if path.IsAbs(ent.Name) {
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(ent.Name)] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve upstream bus directories
 | 
					 | 
				
			||||||
		for _, as := range []string{p.session[0], p.system[0]} {
 | 
					 | 
				
			||||||
			if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
 | 
					 | 
				
			||||||
				// leave / intact
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(as[10:])] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for k := range roBindTarget {
 | 
					 | 
				
			||||||
			bc.Bind(k, k)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
 | 
					 | 
				
			||||||
		p.bwrap = bc
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if output != nil {
 | 
					 | 
				
			||||||
		h.Stdout(output).Stderr(output)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c, cancel := context.WithCancelCause(ctx)
 | 
					 | 
				
			||||||
	if err := h.Start(c, true); err != nil {
 | 
					 | 
				
			||||||
		cancel(err)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	p.helper = h
 | 
					 | 
				
			||||||
	p.ctx = c
 | 
					 | 
				
			||||||
	p.cancel = cancel
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var proxyClosed = errors.New("proxy closed")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Wait blocks until xdg-dbus-proxy exits and releases resources.
 | 
					 | 
				
			||||||
func (p *Proxy) Wait() error {
 | 
					 | 
				
			||||||
	p.lock.RLock()
 | 
					 | 
				
			||||||
	defer p.lock.RUnlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.helper == nil {
 | 
					 | 
				
			||||||
		return errors.New("dbus: not started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	errs := make([]error, 3)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	errs[0] = p.helper.Wait()
 | 
					 | 
				
			||||||
	if p.cancel == nil &&
 | 
					 | 
				
			||||||
		errors.Is(errs[0], context.Canceled) &&
 | 
					 | 
				
			||||||
		errors.Is(context.Cause(p.ctx), proxyClosed) {
 | 
					 | 
				
			||||||
		errs[0] = nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ensure socket removal so ephemeral directory is empty at revert
 | 
					 | 
				
			||||||
	if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
		errs[1] = err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if p.sysP {
 | 
					 | 
				
			||||||
		if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			errs[2] = err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return errors.Join(errs...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
 | 
					 | 
				
			||||||
func (p *Proxy) Close() {
 | 
					 | 
				
			||||||
	p.lock.Lock()
 | 
					 | 
				
			||||||
	defer p.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.cancel == nil {
 | 
					 | 
				
			||||||
		panic("dbus: not started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	p.cancel(proxyClosed)
 | 
					 | 
				
			||||||
	p.cancel = nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -6,6 +6,12 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						sampleHostPath = "/tmp/bus"
 | 
				
			||||||
 | 
						sampleHostAddr = "unix:path=" + sampleHostPath
 | 
				
			||||||
 | 
						sampleBindPath = "/tmp/proxied_bus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var samples = []dbusTestCase{
 | 
					var samples = []dbusTestCase{
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		"org.chromium.Chromium", &dbus.Config{
 | 
							"org.chromium.Chromium", &dbus.Config{
 | 
				
			||||||
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
			"--talk=org.freedesktop.FileManager1",
 | 
								"--talk=org.freedesktop.FileManager1",
 | 
				
			||||||
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{"unix:path=/run/dbus/system_bus_socket",
 | 
							[]string{
 | 
				
			||||||
			"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
 | 
								sampleHostAddr,
 | 
				
			||||||
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.bluez",
 | 
								"--talk=org.bluez",
 | 
				
			||||||
			"--talk=org.freedesktop.Avahi",
 | 
								"--talk=org.freedesktop.Avahi",
 | 
				
			||||||
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
			"--talk=org.kde.StatusNotifierWatcher",
 | 
								"--talk=org.kde.StatusNotifierWatcher",
 | 
				
			||||||
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       true,
 | 
								Log:       true,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--see=uk.gensokyo.CrashTestDummy1",
 | 
								"--see=uk.gensokyo.CrashTestDummy1",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       true,
 | 
								Log:       true,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, true,
 | 
							}, false, true,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--see=uk.gensokyo.CrashTestDummy",
 | 
								"--see=uk.gensokyo.CrashTestDummy",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,4 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestHelperChildStub(t *testing.T) {
 | 
					func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
 | 
				
			||||||
	helper.InternalChildStub()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							@ -10,9 +10,10 @@ cp -rv "comp" "${out}"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
go generate ./...
 | 
					go generate ./...
 | 
				
			||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
 | 
					go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
 | 
				
			||||||
  -X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
 | 
					  -X git.gensokyo.uk/security/fortify/internal.version=${VERSION}
 | 
				
			||||||
  -X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
 | 
					  -X git.gensokyo.uk/security/fortify/internal.fsu=/usr/bin/fsu
 | 
				
			||||||
  -X main.Fmain=/usr/bin/fortify" ./...
 | 
					  -X main.fmain=/usr/bin/fortify
 | 
				
			||||||
 | 
					  -X main.fpkg=/usr/bin/fpkg" ./...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
 | 
					rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
 | 
				
			||||||
rm -rf "./${out}"
 | 
					rm -rf "./${out}"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										46
									
								
								error.go
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								error.go
									
									
									
									
									
								
							@ -1,46 +0,0 @@
 | 
				
			|||||||
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())
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										14
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@ -7,11 +7,11 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1736373539,
 | 
					        "lastModified": 1742234739,
 | 
				
			||||||
        "narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
 | 
					        "narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
 | 
				
			||||||
        "owner": "nix-community",
 | 
					        "owner": "nix-community",
 | 
				
			||||||
        "repo": "home-manager",
 | 
					        "repo": "home-manager",
 | 
				
			||||||
        "rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
 | 
					        "rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
@ -23,16 +23,16 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "nixpkgs": {
 | 
					    "nixpkgs": {
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1739333913,
 | 
					        "lastModified": 1742512142,
 | 
				
			||||||
        "narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
 | 
					        "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
 | 
				
			||||||
        "owner": "NixOS",
 | 
					        "owner": "NixOS",
 | 
				
			||||||
        "repo": "nixpkgs",
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
        "rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
 | 
					        "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
        "owner": "NixOS",
 | 
					        "owner": "NixOS",
 | 
				
			||||||
        "ref": "nixos-24.11-small",
 | 
					        "ref": "nixos-24.11",
 | 
				
			||||||
        "repo": "nixpkgs",
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										134
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								flake.nix
									
									
									
									
									
								
							@ -2,7 +2,7 @@
 | 
				
			|||||||
  description = "fortify sandbox tool and nixos module";
 | 
					  description = "fortify sandbox tool and nixos module";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  inputs = {
 | 
					  inputs = {
 | 
				
			||||||
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
 | 
					    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    home-manager = {
 | 
					    home-manager = {
 | 
				
			||||||
      url = "github:nix-community/home-manager/release-24.11";
 | 
					      url = "github:nix-community/home-manager/release-24.11";
 | 
				
			||||||
@ -27,7 +27,7 @@
 | 
				
			|||||||
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
					      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
				
			||||||
    in
 | 
					    in
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      nixosModules.fortify = import ./nixos.nix;
 | 
					      nixosModules.fortify = import ./nixos.nix self.packages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      buildPackage = forAllSystems (
 | 
					      buildPackage = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
@ -58,6 +58,7 @@
 | 
				
			|||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          fortify = callPackage ./test { inherit system self; };
 | 
					          fortify = callPackage ./test { inherit system self; };
 | 
				
			||||||
 | 
					          fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
 | 
				
			||||||
          race = callPackage ./test {
 | 
					          race = callPackage ./test {
 | 
				
			||||||
            inherit system self;
 | 
					            inherit system self;
 | 
				
			||||||
            withRace = true;
 | 
					            withRace = true;
 | 
				
			||||||
@ -67,7 +68,7 @@
 | 
				
			|||||||
            cd ${./.}
 | 
					            cd ${./.}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            echo "running nixfmt..."
 | 
					            echo "running nixfmt..."
 | 
				
			||||||
            nixfmt --check .
 | 
					            nixfmt --width=256 --check .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            touch $out
 | 
					            touch $out
 | 
				
			||||||
          '';
 | 
					          '';
 | 
				
			||||||
@ -97,115 +98,62 @@
 | 
				
			|||||||
      packages = forAllSystems (
 | 
					      packages = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
        let
 | 
					        let
 | 
				
			||||||
          inherit (self.packages.${system}) fortify;
 | 
					          inherit (self.packages.${system}) fortify fsu;
 | 
				
			||||||
          pkgs = nixpkgsFor.${system};
 | 
					          pkgs = nixpkgsFor.${system};
 | 
				
			||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          default = self.packages.${system}.fortify;
 | 
					          default = fortify;
 | 
				
			||||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
				
			||||||
            inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
					            inherit (pkgs)
 | 
				
			||||||
 | 
					              # passthru.buildInputs
 | 
				
			||||||
 | 
					              go
 | 
				
			||||||
 | 
					              gcc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # nativeBuildInputs
 | 
				
			||||||
 | 
					              pkg-config
 | 
				
			||||||
 | 
					              wayland-scanner
 | 
				
			||||||
 | 
					              makeBinaryWrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # appPackages
 | 
				
			||||||
 | 
					              glibc
 | 
				
			||||||
 | 
					              xdg-dbus-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # fpkg
 | 
				
			||||||
 | 
					              zstd
 | 
				
			||||||
 | 
					              gnutar
 | 
				
			||||||
 | 
					              coreutils
 | 
				
			||||||
 | 
					              ;
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
					          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          dist =
 | 
					          dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
 | 
				
			||||||
            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)"
 | 
				
			||||||
                # 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
 | 
					            # get a different workdir as go does not like /build
 | 
				
			||||||
                cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
 | 
					            cd $(mktemp -d) \
 | 
				
			||||||
 | 
					                && cp -r ${fortify.src}/. . \
 | 
				
			||||||
 | 
					                && chmod +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
 | 
				
			||||||
 | 
					                && chmod -R +w .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                export FORTIFY_VERSION="v${fortify.version}"
 | 
					            export FORTIFY_VERSION="v${fortify.version}"
 | 
				
			||||||
                ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
					            ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
				
			||||||
              '';
 | 
					          '';
 | 
				
			||||||
 | 
					 | 
				
			||||||
          fhs = pkgs.buildFHSEnv {
 | 
					 | 
				
			||||||
            pname = "fortify-fhs";
 | 
					 | 
				
			||||||
            inherit (fortify) version;
 | 
					 | 
				
			||||||
            targetPkgs =
 | 
					 | 
				
			||||||
              pkgs:
 | 
					 | 
				
			||||||
              with pkgs;
 | 
					 | 
				
			||||||
              [
 | 
					 | 
				
			||||||
                go
 | 
					 | 
				
			||||||
                gcc
 | 
					 | 
				
			||||||
                pkg-config
 | 
					 | 
				
			||||||
                wayland-scanner
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
              ++ (
 | 
					 | 
				
			||||||
                with pkgs.pkgsStatic;
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                  musl
 | 
					 | 
				
			||||||
                  libffi
 | 
					 | 
				
			||||||
                  libseccomp
 | 
					 | 
				
			||||||
                  acl
 | 
					 | 
				
			||||||
                  wayland
 | 
					 | 
				
			||||||
                  wayland-protocols
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                ++ (with xorg; [
 | 
					 | 
				
			||||||
                  libxcb
 | 
					 | 
				
			||||||
                  libXau
 | 
					 | 
				
			||||||
                  libXdmcp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  xorgproto
 | 
					 | 
				
			||||||
                ])
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
            extraOutputsToInstall = [ "dev" ];
 | 
					 | 
				
			||||||
            profile = ''
 | 
					 | 
				
			||||||
              export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
 | 
					 | 
				
			||||||
            '';
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      devShells = forAllSystems (
 | 
					      devShells = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
        let
 | 
					        let
 | 
				
			||||||
          inherit (self.packages.${system}) fortify fhs;
 | 
					          inherit (self.packages.${system}) fortify;
 | 
				
			||||||
          pkgs = nixpkgsFor.${system};
 | 
					          pkgs = nixpkgsFor.${system};
 | 
				
			||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          default = pkgs.mkShell {
 | 
					          default = pkgs.mkShell { buildInputs = fortify.targetPkgs; };
 | 
				
			||||||
            buildInputs =
 | 
					          withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
 | 
				
			||||||
              with pkgs;
 | 
					 | 
				
			||||||
              [
 | 
					 | 
				
			||||||
                go
 | 
					 | 
				
			||||||
                gcc
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
              # buildInputs
 | 
					 | 
				
			||||||
              ++ (
 | 
					 | 
				
			||||||
                with pkgsStatic;
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                  musl
 | 
					 | 
				
			||||||
                  libffi
 | 
					 | 
				
			||||||
                  libseccomp
 | 
					 | 
				
			||||||
                  acl
 | 
					 | 
				
			||||||
                  wayland
 | 
					 | 
				
			||||||
                  wayland-protocols
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                ++ (with xorg; [
 | 
					 | 
				
			||||||
                  libxcb
 | 
					 | 
				
			||||||
                  libXau
 | 
					 | 
				
			||||||
                  libXdmcp
 | 
					 | 
				
			||||||
                ])
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
              # nativeBuildInputs
 | 
					 | 
				
			||||||
              ++ [
 | 
					 | 
				
			||||||
                pkg-config
 | 
					 | 
				
			||||||
                wayland-scanner
 | 
					 | 
				
			||||||
                makeBinaryWrapper
 | 
					 | 
				
			||||||
              ];
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          fhs = fhs.env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          withPackage = nixpkgsFor.${system}.mkShell {
 | 
					 | 
				
			||||||
            buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          generateDoc =
 | 
					          generateDoc =
 | 
				
			||||||
            let
 | 
					            let
 | 
				
			||||||
              pkgs = nixpkgsFor.${system};
 | 
					 | 
				
			||||||
              inherit (pkgs) lib;
 | 
					              inherit (pkgs) lib;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              doc =
 | 
					              doc =
 | 
				
			||||||
@ -214,7 +162,7 @@
 | 
				
			|||||||
                    specialArgs = {
 | 
					                    specialArgs = {
 | 
				
			||||||
                      inherit pkgs;
 | 
					                      inherit pkgs;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    modules = [ ./options.nix ];
 | 
					                    modules = [ (import ./options.nix self.packages) ];
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
					                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
@ -224,7 +172,7 @@
 | 
				
			|||||||
                sed -i '/*Declared by:*/,+1 d' $out
 | 
					                sed -i '/*Declared by:*/,+1 d' $out
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
            nixpkgsFor.${system}.mkShell {
 | 
					            pkgs.mkShell {
 | 
				
			||||||
              shellHook = ''
 | 
					              shellHook = ''
 | 
				
			||||||
                exec cat ${docText} > options.md
 | 
					                exec cat ${docText} > options.md
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
package fst
 | 
					package fst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +18,7 @@ type App interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type SealedApp interface {
 | 
					type SealedApp interface {
 | 
				
			||||||
	// Run commits sealed system setup and starts the app process.
 | 
						// Run commits sealed system setup and starts the app process.
 | 
				
			||||||
	Run(ctx context.Context, rs *RunState) error
 | 
						Run(rs *RunState) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RunState stores the outcome of a call to [SealedApp.Run].
 | 
					// RunState stores the outcome of a call to [SealedApp.Run].
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ package fst
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,8 +14,11 @@ type Config struct {
 | 
				
			|||||||
	// passed to wayland security-context-v1 as application ID
 | 
						// passed to wayland security-context-v1 as application ID
 | 
				
			||||||
	// and used as part of defaults in dbus session proxy
 | 
						// and used as part of defaults in dbus session proxy
 | 
				
			||||||
	ID string `json:"id"`
 | 
						ID string `json:"id"`
 | 
				
			||||||
	// final argv, passed to init
 | 
					
 | 
				
			||||||
	Command []string `json:"command"`
 | 
						// absolute path to executable file
 | 
				
			||||||
 | 
						Path string `json:"path,omitempty"`
 | 
				
			||||||
 | 
						// final args passed to container init
 | 
				
			||||||
 | 
						Args []string `json:"args"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Confinement ConfinementConfig `json:"confinement"`
 | 
						Confinement ConfinementConfig `json:"confinement"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -26,13 +29,13 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	AppID int `json:"app_id"`
 | 
						AppID int `json:"app_id"`
 | 
				
			||||||
	// list of supplementary groups to inherit
 | 
						// list of supplementary groups to inherit
 | 
				
			||||||
	Groups []string `json:"groups"`
 | 
						Groups []string `json:"groups"`
 | 
				
			||||||
	// passwd username in the sandbox, defaults to passwd name of target uid or chronos
 | 
						// passwd username in container, defaults to passwd name of target uid or chronos
 | 
				
			||||||
	Username string `json:"username,omitempty"`
 | 
						Username string `json:"username,omitempty"`
 | 
				
			||||||
	// home directory in sandbox, empty for outer
 | 
						// home directory in container, empty for outer
 | 
				
			||||||
	Inner string `json:"home_inner"`
 | 
						Inner string `json:"home_inner"`
 | 
				
			||||||
	// home directory in init namespace
 | 
						// home directory in init namespace
 | 
				
			||||||
	Outer string `json:"home"`
 | 
						Outer string `json:"home"`
 | 
				
			||||||
	// bwrap sandbox confinement configuration
 | 
						// abstract sandbox configuration
 | 
				
			||||||
	Sandbox *SandboxConfig `json:"sandbox"`
 | 
						Sandbox *SandboxConfig `json:"sandbox"`
 | 
				
			||||||
	// extra acl ops, runs after everything else
 | 
						// extra acl ops, runs after everything else
 | 
				
			||||||
	ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
						ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
				
			||||||
@ -44,8 +47,8 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	// nil value makes session bus proxy assume built-in defaults
 | 
						// nil value makes session bus proxy assume built-in defaults
 | 
				
			||||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
						SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// system resources to expose to the sandbox
 | 
						// system resources to expose to the container
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablement `json:"enablements"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ExtraPermConfig struct {
 | 
					type ExtraPermConfig struct {
 | 
				
			||||||
@ -76,24 +79,12 @@ func (e *ExtraPermConfig) String() string {
 | 
				
			|||||||
	return string(buf)
 | 
						return string(buf)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FilesystemConfig struct {
 | 
					 | 
				
			||||||
	// mount point in sandbox, same as src if empty
 | 
					 | 
				
			||||||
	Dst string `json:"dst,omitempty"`
 | 
					 | 
				
			||||||
	// host filesystem path to make available to sandbox
 | 
					 | 
				
			||||||
	Src string `json:"src"`
 | 
					 | 
				
			||||||
	// write access
 | 
					 | 
				
			||||||
	Write bool `json:"write,omitempty"`
 | 
					 | 
				
			||||||
	// device access
 | 
					 | 
				
			||||||
	Device bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// fail if mount fails
 | 
					 | 
				
			||||||
	Must bool `json:"require,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Template returns a fully populated instance of Config.
 | 
					// Template returns a fully populated instance of Config.
 | 
				
			||||||
func Template() *Config {
 | 
					func Template() *Config {
 | 
				
			||||||
	return &Config{
 | 
						return &Config{
 | 
				
			||||||
		ID: "org.chromium.Chromium",
 | 
							ID:   "org.chromium.Chromium",
 | 
				
			||||||
		Command: []string{
 | 
							Path: "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
							Args: []string{
 | 
				
			||||||
			"chromium",
 | 
								"chromium",
 | 
				
			||||||
			"--ignore-gpu-blocklist",
 | 
								"--ignore-gpu-blocklist",
 | 
				
			||||||
			"--disable-smooth-scrolling",
 | 
								"--disable-smooth-scrolling",
 | 
				
			||||||
@ -108,11 +99,13 @@ func Template() *Config {
 | 
				
			|||||||
			Inner:    "/var/lib/fortify",
 | 
								Inner:    "/var/lib/fortify",
 | 
				
			||||||
			Sandbox: &SandboxConfig{
 | 
								Sandbox: &SandboxConfig{
 | 
				
			||||||
				Hostname:      "localhost",
 | 
									Hostname:      "localhost",
 | 
				
			||||||
				UserNS:        true,
 | 
									Devel:         true,
 | 
				
			||||||
 | 
									Userns:        true,
 | 
				
			||||||
				Net:           true,
 | 
									Net:           true,
 | 
				
			||||||
				Dev:           true,
 | 
									Dev:           true,
 | 
				
			||||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
									Seccomp:       seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession:  true,
 | 
									Tty:           true,
 | 
				
			||||||
 | 
									Multiarch:     true,
 | 
				
			||||||
				MapRealUID:    true,
 | 
									MapRealUID:    true,
 | 
				
			||||||
				DirectWayland: false,
 | 
									DirectWayland: false,
 | 
				
			||||||
				// example API credentials pulled from Google Chrome
 | 
									// example API credentials pulled from Google Chrome
 | 
				
			||||||
@ -131,10 +124,10 @@ func Template() *Config {
 | 
				
			|||||||
						Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
											Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
				
			||||||
					{Src: "/dev/dri", Device: true},
 | 
										{Src: "/dev/dri", Device: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
									Link:    [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
				
			||||||
				Etc:      "/etc",
 | 
									Etc:     "/etc",
 | 
				
			||||||
				AutoEtc:  true,
 | 
									AutoEtc: true,
 | 
				
			||||||
				Override: []string{"/var/run/nscd"},
 | 
									Cover:   []string{"/var/run/nscd"},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			ExtraPerms: []*ExtraPermConfig{
 | 
								ExtraPerms: []*ExtraPermConfig{
 | 
				
			||||||
				{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
									{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
				
			||||||
@ -160,7 +153,7 @@ func Template() *Config {
 | 
				
			|||||||
				Log:       false,
 | 
									Log:       false,
 | 
				
			||||||
				Filter:    true,
 | 
									Filter:    true,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
								Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							
							
						
						
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							@ -4,125 +4,149 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxConfig describes resources made available to the sandbox.
 | 
					// SandboxConfig describes resources made available to the sandbox.
 | 
				
			||||||
type SandboxConfig struct {
 | 
					type (
 | 
				
			||||||
	// unix hostname within sandbox
 | 
						SandboxConfig struct {
 | 
				
			||||||
	Hostname string `json:"hostname,omitempty"`
 | 
							// container hostname
 | 
				
			||||||
	// allow userns within sandbox
 | 
							Hostname string `json:"hostname,omitempty"`
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
					 | 
				
			||||||
	// share net namespace
 | 
					 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
					 | 
				
			||||||
	// share all devices
 | 
					 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// seccomp syscall filter policy
 | 
					 | 
				
			||||||
	Syscall *bwrap.SyscallPolicy `json:"syscall"`
 | 
					 | 
				
			||||||
	// do not run in new session
 | 
					 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
					 | 
				
			||||||
	// map target user uid to privileged user uid in the user namespace
 | 
					 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid"`
 | 
					 | 
				
			||||||
	// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
					 | 
				
			||||||
	// and the bare socket is mounted to the sandbox
 | 
					 | 
				
			||||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// final environment variables
 | 
							// extra seccomp flags
 | 
				
			||||||
	Env map[string]string `json:"env"`
 | 
							Seccomp seccomp.SyscallOpts `json:"seccomp"`
 | 
				
			||||||
	// sandbox host filesystem access
 | 
							// allow ptrace and friends
 | 
				
			||||||
	Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
							Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
	// symlinks created inside the sandbox
 | 
							// allow userns creation in container
 | 
				
			||||||
	Link [][2]string `json:"symlink"`
 | 
							Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// read-only /etc directory
 | 
							// share host net namespace
 | 
				
			||||||
	Etc string `json:"etc,omitempty"`
 | 
							Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// automatically set up /etc symlinks
 | 
							// expose main process tty
 | 
				
			||||||
	AutoEtc bool `json:"auto_etc"`
 | 
							Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// mount tmpfs over these paths,
 | 
							// allow multiarch
 | 
				
			||||||
	// runs right before [ConfinementConfig.ExtraPerms]
 | 
							Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	Override []string `json:"override"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
 | 
							// initial process environment variables
 | 
				
			||||||
type SandboxSys interface {
 | 
							Env map[string]string `json:"env"`
 | 
				
			||||||
	Geteuid() int
 | 
							// map target user uid to privileged user uid in the user namespace
 | 
				
			||||||
	Paths() Paths
 | 
							MapRealUID bool `json:"map_real_uid"`
 | 
				
			||||||
	ReadDir(name string) ([]fs.DirEntry, error)
 | 
					 | 
				
			||||||
	EvalSymlinks(path string) (string, error)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Println(v ...any)
 | 
							// expose all devices
 | 
				
			||||||
	Printf(format string, v ...any)
 | 
							Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
}
 | 
							// container host filesystem bind mounts
 | 
				
			||||||
 | 
							Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
				
			||||||
 | 
							// create symlinks inside container filesystem
 | 
				
			||||||
 | 
							Link [][2]string `json:"symlink"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
 | 
							// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
				
			||||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
 | 
							// and the bare socket is mounted to the sandbox
 | 
				
			||||||
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
							DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// read-only /etc directory
 | 
				
			||||||
 | 
							Etc string `json:"etc,omitempty"`
 | 
				
			||||||
 | 
							// automatically set up /etc symlinks
 | 
				
			||||||
 | 
							AutoEtc bool `json:"auto_etc"`
 | 
				
			||||||
 | 
							// cover these paths or create them if they do not already exist
 | 
				
			||||||
 | 
							Cover []string `json:"cover"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
 | 
				
			||||||
 | 
						SandboxSys interface {
 | 
				
			||||||
 | 
							Getuid() int
 | 
				
			||||||
 | 
							Getgid() int
 | 
				
			||||||
 | 
							Paths() Paths
 | 
				
			||||||
 | 
							ReadDir(name string) ([]fs.DirEntry, error)
 | 
				
			||||||
 | 
							EvalSymlinks(path string) (string, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Println(v ...any)
 | 
				
			||||||
 | 
							Printf(format string, v ...any)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FilesystemConfig is a representation of [sandbox.BindMount].
 | 
				
			||||||
 | 
						FilesystemConfig struct {
 | 
				
			||||||
 | 
							// mount point in container, same as src if empty
 | 
				
			||||||
 | 
							Dst string `json:"dst,omitempty"`
 | 
				
			||||||
 | 
							// host filesystem path to make available to the container
 | 
				
			||||||
 | 
							Src string `json:"src"`
 | 
				
			||||||
 | 
							// do not mount filesystem read-only
 | 
				
			||||||
 | 
							Write bool `json:"write,omitempty"`
 | 
				
			||||||
 | 
							// do not disable device files
 | 
				
			||||||
 | 
							Device bool `json:"dev,omitempty"`
 | 
				
			||||||
 | 
							// fail if the bind mount cannot be established for any reason
 | 
				
			||||||
 | 
							Must bool `json:"require,omitempty"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToContainer initialises [sandbox.Params] via [SandboxConfig].
 | 
				
			||||||
 | 
					// Note that remaining container setup must be queued by the [App] implementation.
 | 
				
			||||||
 | 
					func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		return nil, errors.New("nil sandbox config")
 | 
							return nil, nil, syscall.EBADE
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.Syscall == nil {
 | 
						container := &sandbox.Params{
 | 
				
			||||||
		sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !s.MapRealUID {
 | 
					 | 
				
			||||||
		// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
 | 
					 | 
				
			||||||
		*uid = 65534
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// some programs fail to connect to dbus session running as a different uid, so a separate workaround
 | 
					 | 
				
			||||||
		// is introduced to map priv-side caller uid in namespace
 | 
					 | 
				
			||||||
		*uid = sys.Geteuid()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conf := (&bwrap.Config{
 | 
					 | 
				
			||||||
		Net:      s.Net,
 | 
					 | 
				
			||||||
		UserNS:   s.UserNS,
 | 
					 | 
				
			||||||
		UID:      uid,
 | 
					 | 
				
			||||||
		GID:      uid,
 | 
					 | 
				
			||||||
		Hostname: s.Hostname,
 | 
							Hostname: s.Hostname,
 | 
				
			||||||
		Clearenv: true,
 | 
							Ops:      new(sandbox.Ops),
 | 
				
			||||||
		SetEnv:   s.Env,
 | 
							Seccomp:  s.Seccomp,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/* this is only 4 KiB of memory on a 64-bit system,
 | 
						/* this is only 4 KiB of memory on a 64-bit system,
 | 
				
			||||||
		permissive defaults on NixOS results in around 100 entries
 | 
						permissive defaults on NixOS results in around 100 entries
 | 
				
			||||||
		so this capacity should eliminate copies for most setups */
 | 
						so this capacity should eliminate copies for most setups */
 | 
				
			||||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
						*container.Ops = slices.Grow(*container.Ops, 1<<8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Syscall:       s.Syscall,
 | 
						if s.Devel {
 | 
				
			||||||
		NewSession:    !s.NoNewSession,
 | 
							container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
		DieWithParent: true,
 | 
						}
 | 
				
			||||||
		AsInit:        true,
 | 
						if s.Userns {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowUserns
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Net {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowNet
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Tty {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowTTY
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// initialise unconditionally as Once cannot be justified
 | 
						if s.MapRealUID {
 | 
				
			||||||
		// for saving such a miniscule amount of memory
 | 
							/* some programs fail to connect to dbus session running as a different uid
 | 
				
			||||||
		Chmod: make(bwrap.ChmodConfig),
 | 
							so this workaround is introduced to map priv-side caller uid in container */
 | 
				
			||||||
	}).
 | 
							container.Uid = sys.Getuid()
 | 
				
			||||||
		Procfs("/proc").
 | 
							*uid = container.Uid
 | 
				
			||||||
		Tmpfs(Tmp, 4*1024)
 | 
							container.Gid = sys.Getgid()
 | 
				
			||||||
 | 
							*gid = container.Gid
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							*uid = sandbox.OverflowUid()
 | 
				
			||||||
 | 
							*gid = sandbox.OverflowGid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						container.
 | 
				
			||||||
 | 
							Proc("/proc").
 | 
				
			||||||
 | 
							Tmpfs(Tmp, 1<<12, 0755)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.Dev {
 | 
						if !s.Dev {
 | 
				
			||||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
							container.Dev("/dev").Mqueue("/dev/mqueue")
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
							container.Bind("/dev", "/dev", sandbox.BindDevice)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.AutoEtc {
 | 
						/* retrieve paths and hide them if they're made available in the sandbox;
 | 
				
			||||||
		if s.Etc == "" {
 | 
						this feature tries to improve user experience of permissive defaults, and
 | 
				
			||||||
			conf.Dir("/etc")
 | 
						to warn about issues in custom configuration; it is NOT a security feature
 | 
				
			||||||
		} else {
 | 
						and should not be treated as such, ALWAYS be careful with what you bind */
 | 
				
			||||||
			conf.Bind(s.Etc, "/etc")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// retrieve paths and hide them if they're made available in the sandbox
 | 
					 | 
				
			||||||
	var hidePaths []string
 | 
						var hidePaths []string
 | 
				
			||||||
	sc := sys.Paths()
 | 
						sc := sys.Paths()
 | 
				
			||||||
	hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
						hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
				
			||||||
	_, systemBusAddr := dbus.Address()
 | 
						_, systemBusAddr := dbus.Address()
 | 
				
			||||||
	if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
						if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// there is usually only one, do not preallocate
 | 
							// there is usually only one, do not preallocate
 | 
				
			||||||
		for _, entry := range entries {
 | 
							for _, entry := range entries {
 | 
				
			||||||
@ -148,7 +172,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
	hidePathMatch := make([]bool, len(hidePaths))
 | 
						hidePathMatch := make([]bool, len(hidePaths))
 | 
				
			||||||
	for i := range hidePaths {
 | 
						for i := range hidePaths {
 | 
				
			||||||
		if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
							if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -158,19 +182,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !path.IsAbs(c.Src) {
 | 
							if !path.IsAbs(c.Src) {
 | 
				
			||||||
			return nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
								return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		dest := c.Dst
 | 
							dest := c.Dst
 | 
				
			||||||
		if c.Dst == "" {
 | 
							if c.Dst == "" {
 | 
				
			||||||
			dest = c.Src
 | 
								dest = c.Src
 | 
				
			||||||
		} else if !path.IsAbs(dest) {
 | 
							} else if !path.IsAbs(dest) {
 | 
				
			||||||
			return nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
								return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		srcH := c.Src
 | 
							srcH := c.Src
 | 
				
			||||||
		if err := evalSymlinks(sys, &srcH); err != nil {
 | 
							if err := evalSymlinks(sys, &srcH); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for i := range hidePaths {
 | 
							for i := range hidePaths {
 | 
				
			||||||
@ -180,54 +204,69 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
								if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
				
			||||||
				return nil, err
 | 
									return nil, nil, err
 | 
				
			||||||
			} else if ok {
 | 
								} else if ok {
 | 
				
			||||||
				hidePathMatch[i] = true
 | 
									hidePathMatch[i] = true
 | 
				
			||||||
				sys.Printf("hiding paths from %q", c.Src)
 | 
									sys.Printf("hiding paths from %q", c.Src)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
 | 
							var flags int
 | 
				
			||||||
 | 
							if c.Write {
 | 
				
			||||||
 | 
								flags |= sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.Device {
 | 
				
			||||||
 | 
								flags |= sandbox.BindDevice | sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !c.Must {
 | 
				
			||||||
 | 
								flags |= sandbox.BindOptional
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.Bind(c.Src, dest, flags)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// hide marked paths before setting up shares
 | 
						// cover matched paths
 | 
				
			||||||
	for i, ok := range hidePathMatch {
 | 
						for i, ok := range hidePathMatch {
 | 
				
			||||||
		if ok {
 | 
							if ok {
 | 
				
			||||||
			conf.Tmpfs(hidePaths[i], 8192)
 | 
								container.Tmpfs(hidePaths[i], 1<<13, 0755)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, l := range s.Link {
 | 
						for _, l := range s.Link {
 | 
				
			||||||
		conf.Symlink(l[0], l[1])
 | 
							container.Link(l[0], l[1])
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.AutoEtc {
 | 
						// perf: this might work better if implemented as a setup op in container init
 | 
				
			||||||
		etc := s.Etc
 | 
						if !s.AutoEtc {
 | 
				
			||||||
		if etc == "" {
 | 
							if s.Etc != "" {
 | 
				
			||||||
			etc = "/etc"
 | 
								container.Bind(s.Etc, "/etc", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		conf.Bind(etc, Tmp+"/etc")
 | 
						} else {
 | 
				
			||||||
 | 
							etcPath := s.Etc
 | 
				
			||||||
 | 
							if etcPath == "" {
 | 
				
			||||||
 | 
								etcPath = "/etc"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.Bind(etcPath, Tmp+"/etc", 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
							// link host /etc contents to prevent dropping passwd/group bind mounts
 | 
				
			||||||
		if d, err := sys.ReadDir(etc); err != nil {
 | 
							if d, err := sys.ReadDir(etcPath); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			for _, ent := range d {
 | 
								for _, ent := range d {
 | 
				
			||||||
				name := ent.Name()
 | 
									n := ent.Name()
 | 
				
			||||||
				switch name {
 | 
									switch n {
 | 
				
			||||||
				case "passwd":
 | 
									case "passwd":
 | 
				
			||||||
				case "group":
 | 
									case "group":
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case "mtab":
 | 
									case "mtab":
 | 
				
			||||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
										container.Link("/proc/mounts", "/etc/"+n)
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
										container.Link(Tmp+"/etc/"+n, "/etc/"+n)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return conf, nil
 | 
						return container, maps.Clone(s.Env), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func evalSymlinks(sys SandboxSys, v *string) error {
 | 
					func evalSymlinks(sys SandboxSys, v *string) error {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -1,3 +1,3 @@
 | 
				
			|||||||
module git.gensokyo.uk/security/fortify
 | 
					module git.gensokyo.uk/security/fortify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.22
 | 
					go 1.23
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Test_argsFD_String(t *testing.T) {
 | 
					func Test_argsFd_String(t *testing.T) {
 | 
				
			||||||
	wantString := strings.Join(wantArgs, " ")
 | 
						wantString := strings.Join(wantArgs, " ")
 | 
				
			||||||
	if got := argsWt.(fmt.Stringer).String(); got != wantString {
 | 
						if got := argsWt.(fmt.Stringer).String(); got != wantString {
 | 
				
			||||||
		t.Errorf("String(): got %v; want %v",
 | 
							t.Errorf("String(): got %v; want %v",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,87 +0,0 @@
 | 
				
			|||||||
package helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// BubblewrapName is the file name or path to bubblewrap.
 | 
					 | 
				
			||||||
var BubblewrapName = "bwrap"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type bubblewrap struct {
 | 
					 | 
				
			||||||
	// final args fd of bwrap process
 | 
					 | 
				
			||||||
	argsFd uintptr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// name of the command to run in bwrap
 | 
					 | 
				
			||||||
	name string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	lock sync.RWMutex
 | 
					 | 
				
			||||||
	*helperCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
 | 
					 | 
				
			||||||
	b.lock.Lock()
 | 
					 | 
				
			||||||
	defer b.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
					 | 
				
			||||||
	// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
					 | 
				
			||||||
	if b.Cmd != nil && b.Cmd.Process != nil {
 | 
					 | 
				
			||||||
		return errors.New("exec: already started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args := b.finalise(ctx, stat)
 | 
					 | 
				
			||||||
	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...)
 | 
					 | 
				
			||||||
	return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// 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,
 | 
					 | 
				
			||||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
					 | 
				
			||||||
	extraFiles []*os.File,
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) Helper {
 | 
					 | 
				
			||||||
	b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		panic(err.Error())
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return b
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// 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,
 | 
					 | 
				
			||||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
					 | 
				
			||||||
	extraFiles []*os.File,
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) (Helper, error) {
 | 
					 | 
				
			||||||
	b := new(bubblewrap)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	b.name = name
 | 
					 | 
				
			||||||
	b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		f := proc.NewWriterTo(v)
 | 
					 | 
				
			||||||
		b.argsFd = proc.InitFile(f, b.extraFiles)
 | 
					 | 
				
			||||||
		b.files = append(b.files, f)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return b, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,72 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Builder interface {
 | 
					 | 
				
			||||||
	Len() int
 | 
					 | 
				
			||||||
	Append(args *[]string)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type FSBuilder interface {
 | 
					 | 
				
			||||||
	Path() string
 | 
					 | 
				
			||||||
	Builder
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type FDBuilder interface {
 | 
					 | 
				
			||||||
	proc.File
 | 
					 | 
				
			||||||
	Builder
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Args returns a slice of bwrap args corresponding to c.
 | 
					 | 
				
			||||||
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
 | 
					 | 
				
			||||||
	builders := []Builder{
 | 
					 | 
				
			||||||
		c.boolArgs(),
 | 
					 | 
				
			||||||
		c.intArgs(),
 | 
					 | 
				
			||||||
		c.stringArgs(),
 | 
					 | 
				
			||||||
		c.pairArgs(),
 | 
					 | 
				
			||||||
		c.seccompArgs(),
 | 
					 | 
				
			||||||
		newFile(SyncFd.String(), syncFd),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	builders = slices.Grow(builders, len(c.Filesystem)+1)
 | 
					 | 
				
			||||||
	for _, f := range c.Filesystem {
 | 
					 | 
				
			||||||
		builders = append(builders, f)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	builders = append(builders, c.Chmod)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	argc := 0
 | 
					 | 
				
			||||||
	fc := 0
 | 
					 | 
				
			||||||
	for _, b := range builders {
 | 
					 | 
				
			||||||
		l := b.Len()
 | 
					 | 
				
			||||||
		if l < 1 {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		argc += l
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if f, ok := b.(FDBuilder); ok {
 | 
					 | 
				
			||||||
			fc++
 | 
					 | 
				
			||||||
			proc.InitFile(f, extraFiles)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fc++ // allocate extra slot for stat fd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args = make([]string, 0, argc)
 | 
					 | 
				
			||||||
	*files = slices.Grow(*files, fc)
 | 
					 | 
				
			||||||
	for _, b := range builders {
 | 
					 | 
				
			||||||
		if b.Len() < 1 {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		b.Append(&args)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if f, ok := b.(FDBuilder); ok {
 | 
					 | 
				
			||||||
			*files = append(*files, f)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,199 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
Bind binds mount src on host to dest in sandbox.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest) bind mount host path readonly on sandbox
 | 
					 | 
				
			||||||
(--ro-bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--ro-bind-try SRC DEST).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest, false, true) bind mount host path on sandbox.
 | 
					 | 
				
			||||||
(--bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--bind-try SRC DEST).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
 | 
					 | 
				
			||||||
(--dev-bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--dev-bind-try SRC DEST).
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		try   bool
 | 
					 | 
				
			||||||
		write bool
 | 
					 | 
				
			||||||
		dev   bool
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(opts) > 0 {
 | 
					 | 
				
			||||||
		try = opts[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(opts) > 1 {
 | 
					 | 
				
			||||||
		write = opts[1]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(opts) > 2 {
 | 
					 | 
				
			||||||
		dev = opts[2]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if dev {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	} else if write {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// WriteFile copy from FD to destination DEST
 | 
					 | 
				
			||||||
// (--file FD DEST)
 | 
					 | 
				
			||||||
func (c *Config) WriteFile(name string, data []byte) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, Type: DataWrite})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
CopyBind copy from FD to file which is readonly bind-mounted on DEST
 | 
					 | 
				
			||||||
(--ro-bind-data FD DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
 | 
					 | 
				
			||||||
(--bind-data FD DEST)
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	var p *[]byte
 | 
					 | 
				
			||||||
	c.CopyBindRef(dest, &p, opts...)
 | 
					 | 
				
			||||||
	*p = payload
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data.
 | 
					 | 
				
			||||||
func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	t := DataROBind
 | 
					 | 
				
			||||||
	if len(opts) > 0 && opts[0] {
 | 
					 | 
				
			||||||
		t = DataBind
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	d := &DataConfig{Dest: dest, Type: t}
 | 
					 | 
				
			||||||
	*payloadRef = &d.Data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, d)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Dir create dir in sandbox
 | 
					 | 
				
			||||||
// (--dir DEST)
 | 
					 | 
				
			||||||
func (c *Config) Dir(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// RemountRO remount path as readonly; does not recursively remount
 | 
					 | 
				
			||||||
// (--remount-ro DEST)
 | 
					 | 
				
			||||||
func (c *Config) RemountRO(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Procfs mount new procfs in sandbox
 | 
					 | 
				
			||||||
// (--proc DEST)
 | 
					 | 
				
			||||||
func (c *Config) Procfs(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DevTmpfs mount new dev in sandbox
 | 
					 | 
				
			||||||
// (--dev DEST)
 | 
					 | 
				
			||||||
func (c *Config) DevTmpfs(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Mqueue mount new mqueue in sandbox
 | 
					 | 
				
			||||||
// (--mqueue DEST)
 | 
					 | 
				
			||||||
func (c *Config) Mqueue(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Tmpfs mount new tmpfs in sandbox
 | 
					 | 
				
			||||||
// (--tmpfs DEST)
 | 
					 | 
				
			||||||
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
 | 
					 | 
				
			||||||
	tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
 | 
					 | 
				
			||||||
	if size >= 0 {
 | 
					 | 
				
			||||||
		tmpfs.Inner.Size = size
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(perm) == 1 {
 | 
					 | 
				
			||||||
		tmpfs.Mode = &perm[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, tmpfs)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
 | 
					 | 
				
			||||||
// (--tmp-overlay DEST)
 | 
					 | 
				
			||||||
func (c *Config) Overlay(dest string, src ...string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Join mount overlayfs read-only on DEST
 | 
					 | 
				
			||||||
// (--ro-overlay DEST)
 | 
					 | 
				
			||||||
func (c *Config) Join(dest string, src ...string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
 | 
					 | 
				
			||||||
// WORKDIR an empty directory on the same filesystem as RWSRC
 | 
					 | 
				
			||||||
// (--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
 | 
					 | 
				
			||||||
	if rwsrc == "" || workdir == "" {
 | 
					 | 
				
			||||||
		panic("persist called without required paths")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Symlink create symlink within sandbox
 | 
					 | 
				
			||||||
// (--symlink SRC DEST)
 | 
					 | 
				
			||||||
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
 | 
					 | 
				
			||||||
	symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
 | 
					 | 
				
			||||||
	if len(perm) == 1 {
 | 
					 | 
				
			||||||
		symlink.Mode = &perm[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, symlink)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
 | 
					 | 
				
			||||||
func (c *Config) SetUID(uid int) *Config {
 | 
					 | 
				
			||||||
	if uid >= 0 {
 | 
					 | 
				
			||||||
		c.UID = &uid
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
 | 
					 | 
				
			||||||
func (c *Config) SetGID(gid int) *Config {
 | 
					 | 
				
			||||||
	if gid >= 0 {
 | 
					 | 
				
			||||||
		c.GID = &gid
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,104 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Config struct {
 | 
					 | 
				
			||||||
	// unshare every namespace we support by default if nil
 | 
					 | 
				
			||||||
	// (--unshare-all)
 | 
					 | 
				
			||||||
	Unshare *UnshareConfig `json:"unshare,omitempty"`
 | 
					 | 
				
			||||||
	// retain the network namespace (can only combine with nil Unshare)
 | 
					 | 
				
			||||||
	// (--share-net)
 | 
					 | 
				
			||||||
	Net bool `json:"net"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// disable further use of user namespaces inside sandbox and fail unless
 | 
					 | 
				
			||||||
	// further use of user namespace inside sandbox is disabled if false
 | 
					 | 
				
			||||||
	// (--disable-userns) (--assert-userns-disabled)
 | 
					 | 
				
			||||||
	UserNS bool `json:"userns"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// custom uid in the sandbox, requires new user namespace
 | 
					 | 
				
			||||||
	// (--uid UID)
 | 
					 | 
				
			||||||
	UID *int `json:"uid,omitempty"`
 | 
					 | 
				
			||||||
	// custom gid in the sandbox, requires new user namespace
 | 
					 | 
				
			||||||
	// (--gid GID)
 | 
					 | 
				
			||||||
	GID *int `json:"gid,omitempty"`
 | 
					 | 
				
			||||||
	// custom hostname in the sandbox, requires new uts namespace
 | 
					 | 
				
			||||||
	// (--hostname NAME)
 | 
					 | 
				
			||||||
	Hostname string `json:"hostname,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// change directory
 | 
					 | 
				
			||||||
	// (--chdir DIR)
 | 
					 | 
				
			||||||
	Chdir string `json:"chdir,omitempty"`
 | 
					 | 
				
			||||||
	// unset all environment variables
 | 
					 | 
				
			||||||
	// (--clearenv)
 | 
					 | 
				
			||||||
	Clearenv bool `json:"clearenv"`
 | 
					 | 
				
			||||||
	// set environment variable
 | 
					 | 
				
			||||||
	// (--setenv VAR VALUE)
 | 
					 | 
				
			||||||
	SetEnv map[string]string `json:"setenv,omitempty"`
 | 
					 | 
				
			||||||
	// unset environment variables
 | 
					 | 
				
			||||||
	// (--unsetenv VAR)
 | 
					 | 
				
			||||||
	UnsetEnv []string `json:"unsetenv,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// take a lock on file while sandbox is running
 | 
					 | 
				
			||||||
	// (--lock-file DEST)
 | 
					 | 
				
			||||||
	LockFile []string `json:"lock_file,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ordered filesystem args
 | 
					 | 
				
			||||||
	Filesystem []FSBuilder `json:"filesystem,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// change permissions (must already exist)
 | 
					 | 
				
			||||||
	// (--chmod OCTAL PATH)
 | 
					 | 
				
			||||||
	Chmod ChmodConfig `json:"chmod,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// load and use seccomp rules from FD (not repeatable)
 | 
					 | 
				
			||||||
	// (--seccomp FD)
 | 
					 | 
				
			||||||
	Syscall *SyscallPolicy
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// create a new terminal session
 | 
					 | 
				
			||||||
	// (--new-session)
 | 
					 | 
				
			||||||
	NewSession bool `json:"new_session"`
 | 
					 | 
				
			||||||
	// kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
 | 
					 | 
				
			||||||
	// (--die-with-parent)
 | 
					 | 
				
			||||||
	DieWithParent bool `json:"die_with_parent"`
 | 
					 | 
				
			||||||
	// do not install a reaper process with PID=1
 | 
					 | 
				
			||||||
	// (--as-pid-1)
 | 
					 | 
				
			||||||
	AsInit bool `json:"as_init"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/* unmapped options include:
 | 
					 | 
				
			||||||
	    --unshare-user-try           Create new user namespace if possible else continue by skipping it
 | 
					 | 
				
			||||||
	    --unshare-cgroup-try         Create new cgroup namespace if possible else continue by skipping it
 | 
					 | 
				
			||||||
	    --userns FD                  Use this user namespace (cannot combine with --unshare-user)
 | 
					 | 
				
			||||||
	    --userns2 FD                 After setup switch to this user namespace, only useful with --userns
 | 
					 | 
				
			||||||
	    --pidns FD                   Use this pid namespace (as parent namespace if using --unshare-pid)
 | 
					 | 
				
			||||||
	    --bind-fd FD DEST            Bind open directory or path fd on DEST
 | 
					 | 
				
			||||||
	    --ro-bind-fd FD DEST         Bind open directory or path fd read-only on DEST
 | 
					 | 
				
			||||||
	    --exec-label LABEL           Exec label for the sandbox
 | 
					 | 
				
			||||||
	    --file-label LABEL           File label for temporary sandbox content
 | 
					 | 
				
			||||||
	    --add-seccomp-fd FD          Load and use seccomp rules from FD (repeatable)
 | 
					 | 
				
			||||||
	    --block-fd FD                Block on FD until some data to read is available
 | 
					 | 
				
			||||||
	    --userns-block-fd FD         Block on FD until the user namespace is ready
 | 
					 | 
				
			||||||
	    --info-fd FD                 Write information about the running container to FD
 | 
					 | 
				
			||||||
	    --json-status-fd FD          Write container status to FD as multiple JSON documents
 | 
					 | 
				
			||||||
	    --cap-add CAP                Add cap CAP when running as privileged user
 | 
					 | 
				
			||||||
	    --cap-drop CAP               Drop cap CAP when running as privileged user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	among which --args is used internally for passing arguments */
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type UnshareConfig struct {
 | 
					 | 
				
			||||||
	// (--unshare-user)
 | 
					 | 
				
			||||||
	// create new user namespace
 | 
					 | 
				
			||||||
	User bool `json:"user"`
 | 
					 | 
				
			||||||
	// (--unshare-ipc)
 | 
					 | 
				
			||||||
	// create new ipc namespace
 | 
					 | 
				
			||||||
	IPC bool `json:"ipc"`
 | 
					 | 
				
			||||||
	// (--unshare-pid)
 | 
					 | 
				
			||||||
	// create new pid namespace
 | 
					 | 
				
			||||||
	PID bool `json:"pid"`
 | 
					 | 
				
			||||||
	// (--unshare-net)
 | 
					 | 
				
			||||||
	// create new network namespace
 | 
					 | 
				
			||||||
	Net bool `json:"net"`
 | 
					 | 
				
			||||||
	// (--unshare-uts)
 | 
					 | 
				
			||||||
	// create new uts namespace
 | 
					 | 
				
			||||||
	UTS bool `json:"uts"`
 | 
					 | 
				
			||||||
	// (--unshare-cgroup)
 | 
					 | 
				
			||||||
	// create new cgroup namespace
 | 
					 | 
				
			||||||
	CGroup bool `json:"cgroup"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,257 +0,0 @@
 | 
				
			|||||||
package bwrap_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestConfig_Args(t *testing.T) {
 | 
					 | 
				
			||||||
	seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
	t.Cleanup(func() { seccomp.CPrintln = nil })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testCases := []struct {
 | 
					 | 
				
			||||||
		name string
 | 
					 | 
				
			||||||
		conf *bwrap.Config
 | 
					 | 
				
			||||||
		want []string
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"bind", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Bind("/etc", "/.fortify/etc").
 | 
					 | 
				
			||||||
				Bind("/etc", "/.fortify/etc", true).
 | 
					 | 
				
			||||||
				Bind("/run", "/.fortify/run", false, true).
 | 
					 | 
				
			||||||
				Bind("/sys/devices", "/.fortify/sys/devices", true, true).
 | 
					 | 
				
			||||||
				Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
 | 
					 | 
				
			||||||
				Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Bind("/etc", "/.fortify/etc")
 | 
					 | 
				
			||||||
				"--ro-bind", "/etc", "/.fortify/etc",
 | 
					 | 
				
			||||||
				// Bind("/etc", "/.fortify/etc", true)
 | 
					 | 
				
			||||||
				"--ro-bind-try", "/etc", "/.fortify/etc",
 | 
					 | 
				
			||||||
				// Bind("/run", "/.fortify/run", false, true)
 | 
					 | 
				
			||||||
				"--bind", "/run", "/.fortify/run",
 | 
					 | 
				
			||||||
				// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
 | 
					 | 
				
			||||||
				"--bind-try", "/sys/devices", "/.fortify/sys/devices",
 | 
					 | 
				
			||||||
				// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
 | 
					 | 
				
			||||||
				"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
 | 
					 | 
				
			||||||
				// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
 | 
					 | 
				
			||||||
				"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Dir("/.fortify").
 | 
					 | 
				
			||||||
				RemountRO("/home").
 | 
					 | 
				
			||||||
				Procfs("/proc").
 | 
					 | 
				
			||||||
				DevTmpfs("/dev").
 | 
					 | 
				
			||||||
				Mqueue("/dev/mqueue"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Dir("/.fortify")
 | 
					 | 
				
			||||||
				"--dir", "/.fortify",
 | 
					 | 
				
			||||||
				// RemountRO("/home")
 | 
					 | 
				
			||||||
				"--remount-ro", "/home",
 | 
					 | 
				
			||||||
				// Procfs("/proc")
 | 
					 | 
				
			||||||
				"--proc", "/proc",
 | 
					 | 
				
			||||||
				// DevTmpfs("/dev")
 | 
					 | 
				
			||||||
				"--dev", "/dev",
 | 
					 | 
				
			||||||
				// Mqueue("/dev/mqueue")
 | 
					 | 
				
			||||||
				"--mqueue", "/dev/mqueue",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"tmpfs", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Tmpfs("/run/user", 8192).
 | 
					 | 
				
			||||||
				Tmpfs("/run/dbus", 8192, 0755),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Tmpfs("/run/user", 8192)
 | 
					 | 
				
			||||||
				"--size", "8192", "--tmpfs", "/run/user",
 | 
					 | 
				
			||||||
				// Tmpfs("/run/dbus", 8192, 0755)
 | 
					 | 
				
			||||||
				"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"symlink", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Symlink("/.fortify/sbin/init", "/sbin/init").
 | 
					 | 
				
			||||||
				Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Symlink("/.fortify/sbin/init", "/sbin/init")
 | 
					 | 
				
			||||||
				"--symlink", "/.fortify/sbin/init", "/sbin/init",
 | 
					 | 
				
			||||||
				// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
 | 
					 | 
				
			||||||
				"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"overlayfs", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Overlay("/etc", "/etc").
 | 
					 | 
				
			||||||
				Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
 | 
					 | 
				
			||||||
				Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Overlay("/etc", "/etc")
 | 
					 | 
				
			||||||
				"--overlay-src", "/etc", "--tmp-overlay", "/etc",
 | 
					 | 
				
			||||||
				// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
 | 
					 | 
				
			||||||
				"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
 | 
					 | 
				
			||||||
				"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
 | 
					 | 
				
			||||||
				// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
 | 
					 | 
				
			||||||
				"--overlay-src", "/data/app/org.chromium.Chromium/nix",
 | 
					 | 
				
			||||||
				"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"copy", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				WriteFile("/.fortify/version", make([]byte, 8)).
 | 
					 | 
				
			||||||
				CopyBind("/etc/group", make([]byte, 8)).
 | 
					 | 
				
			||||||
				CopyBind("/etc/passwd", make([]byte, 8), true),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Write("/.fortify/version", make([]byte, 8))
 | 
					 | 
				
			||||||
				"--file", "3", "/.fortify/version",
 | 
					 | 
				
			||||||
				// CopyBind("/etc/group", make([]byte, 8))
 | 
					 | 
				
			||||||
				"--ro-bind-data", "4", "/etc/group",
 | 
					 | 
				
			||||||
				// CopyBind("/etc/passwd", make([]byte, 8), true)
 | 
					 | 
				
			||||||
				"--bind-data", "5", "/etc/passwd",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
 | 
					 | 
				
			||||||
				User:   false,
 | 
					 | 
				
			||||||
				IPC:    false,
 | 
					 | 
				
			||||||
				PID:    false,
 | 
					 | 
				
			||||||
				Net:    false,
 | 
					 | 
				
			||||||
				UTS:    false,
 | 
					 | 
				
			||||||
				CGroup: false,
 | 
					 | 
				
			||||||
			}},
 | 
					 | 
				
			||||||
			[]string{"--disable-userns", "--assert-userns-disabled"},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"uid gid sync", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				SetUID(1971).
 | 
					 | 
				
			||||||
				SetGID(100),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// SetUID(1971)
 | 
					 | 
				
			||||||
				"--uid", "1971",
 | 
					 | 
				
			||||||
				// SetGID(100)
 | 
					 | 
				
			||||||
				"--gid", "100",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
 | 
					 | 
				
			||||||
				Hostname: "fortify",
 | 
					 | 
				
			||||||
				Chdir:    "/.fortify",
 | 
					 | 
				
			||||||
				SetEnv:   map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
 | 
					 | 
				
			||||||
				UnsetEnv: []string{"HOME", "HOST"},
 | 
					 | 
				
			||||||
				LockFile: []string{"/.fortify/lock"},
 | 
					 | 
				
			||||||
				Syscall:  new(bwrap.SyscallPolicy),
 | 
					 | 
				
			||||||
				Chmod:    map[string]os.FileMode{"/.fortify/sbin/init": 0755},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Hostname: "fortify"
 | 
					 | 
				
			||||||
				"--hostname", "fortify",
 | 
					 | 
				
			||||||
				// Chdir: "/.fortify"
 | 
					 | 
				
			||||||
				"--chdir", "/.fortify",
 | 
					 | 
				
			||||||
				// UnsetEnv: []string{"HOME", "HOST"}
 | 
					 | 
				
			||||||
				"--unsetenv", "HOME",
 | 
					 | 
				
			||||||
				"--unsetenv", "HOST",
 | 
					 | 
				
			||||||
				// LockFile: []string{"/.fortify/lock"},
 | 
					 | 
				
			||||||
				"--lock-file", "/.fortify/lock",
 | 
					 | 
				
			||||||
				// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
 | 
					 | 
				
			||||||
				"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
 | 
					 | 
				
			||||||
				// Syscall: new(bwrap.SyscallPolicy),
 | 
					 | 
				
			||||||
				"--seccomp", "3",
 | 
					 | 
				
			||||||
				// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
 | 
					 | 
				
			||||||
				"--chmod", "755", "/.fortify/sbin/init",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
 | 
					 | 
				
			||||||
				Symlink("usr/bin", "/bin").
 | 
					 | 
				
			||||||
				Symlink("var/home", "/home").
 | 
					 | 
				
			||||||
				Symlink("usr/lib", "/lib").
 | 
					 | 
				
			||||||
				Symlink("usr/lib64", "/lib64").
 | 
					 | 
				
			||||||
				Symlink("run/media", "/media").
 | 
					 | 
				
			||||||
				Symlink("var/mnt", "/mnt").
 | 
					 | 
				
			||||||
				Symlink("var/opt", "/opt").
 | 
					 | 
				
			||||||
				Symlink("sysroot/ostree", "/ostree").
 | 
					 | 
				
			||||||
				Symlink("var/roothome", "/root").
 | 
					 | 
				
			||||||
				Symlink("usr/sbin", "/sbin").
 | 
					 | 
				
			||||||
				Symlink("var/srv", "/srv").
 | 
					 | 
				
			||||||
				Bind("/run", "/run", false, true).
 | 
					 | 
				
			||||||
				Bind("/tmp", "/tmp", false, true).
 | 
					 | 
				
			||||||
				Bind("/var", "/var", false, true).
 | 
					 | 
				
			||||||
				Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
 | 
					 | 
				
			||||||
				Bind("/boot", "/boot").
 | 
					 | 
				
			||||||
				Bind("/dev", "/dev").
 | 
					 | 
				
			||||||
				Bind("/proc", "/proc").
 | 
					 | 
				
			||||||
				Bind("/sys", "/sys").
 | 
					 | 
				
			||||||
				Bind("/sysroot", "/sysroot").
 | 
					 | 
				
			||||||
				Bind("/usr", "/usr").
 | 
					 | 
				
			||||||
				Bind("/etc", "/etc"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				"--clearenv", "--die-with-parent",
 | 
					 | 
				
			||||||
				"--symlink", "usr/bin", "/bin",
 | 
					 | 
				
			||||||
				"--symlink", "var/home", "/home",
 | 
					 | 
				
			||||||
				"--symlink", "usr/lib", "/lib",
 | 
					 | 
				
			||||||
				"--symlink", "usr/lib64", "/lib64",
 | 
					 | 
				
			||||||
				"--symlink", "run/media", "/media",
 | 
					 | 
				
			||||||
				"--symlink", "var/mnt", "/mnt",
 | 
					 | 
				
			||||||
				"--symlink", "var/opt", "/opt",
 | 
					 | 
				
			||||||
				"--symlink", "sysroot/ostree", "/ostree",
 | 
					 | 
				
			||||||
				"--symlink", "var/roothome", "/root",
 | 
					 | 
				
			||||||
				"--symlink", "usr/sbin", "/sbin",
 | 
					 | 
				
			||||||
				"--symlink", "var/srv", "/srv",
 | 
					 | 
				
			||||||
				"--bind", "/run", "/run",
 | 
					 | 
				
			||||||
				"--bind", "/tmp", "/tmp",
 | 
					 | 
				
			||||||
				"--bind", "/var", "/var",
 | 
					 | 
				
			||||||
				"--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
 | 
					 | 
				
			||||||
				"--ro-bind", "/boot", "/boot",
 | 
					 | 
				
			||||||
				"--ro-bind", "/dev", "/dev",
 | 
					 | 
				
			||||||
				"--ro-bind", "/proc", "/proc",
 | 
					 | 
				
			||||||
				"--ro-bind", "/sys", "/sys",
 | 
					 | 
				
			||||||
				"--ro-bind", "/sysroot", "/sysroot",
 | 
					 | 
				
			||||||
				"--ro-bind", "/usr", "/usr",
 | 
					 | 
				
			||||||
				"--ro-bind", "/etc", "/etc",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, tc := range testCases {
 | 
					 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
					 | 
				
			||||||
			if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
 | 
					 | 
				
			||||||
				t.Errorf("Args() = %#v, want %#v", got, tc.want)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// test persist validation
 | 
					 | 
				
			||||||
	t.Run("invalid persist", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer func() {
 | 
					 | 
				
			||||||
			wantPanic := "persist called without required paths"
 | 
					 | 
				
			||||||
			if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
				t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
		(new(bwrap.Config)).Persist("/run", "", "")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,85 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SyscallPolicy struct {
 | 
					 | 
				
			||||||
	// disable fortify extensions
 | 
					 | 
				
			||||||
	Compat bool `json:"compat"`
 | 
					 | 
				
			||||||
	// deny development syscalls
 | 
					 | 
				
			||||||
	DenyDevel bool `json:"deny_devel"`
 | 
					 | 
				
			||||||
	// deny multiarch/emulation syscalls
 | 
					 | 
				
			||||||
	Multiarch bool `json:"multiarch"`
 | 
					 | 
				
			||||||
	// allow PER_LINUX32
 | 
					 | 
				
			||||||
	Linux32 bool `json:"linux32"`
 | 
					 | 
				
			||||||
	// allow AF_CAN
 | 
					 | 
				
			||||||
	Can bool `json:"can"`
 | 
					 | 
				
			||||||
	// allow AF_BLUETOOTH
 | 
					 | 
				
			||||||
	Bluetooth bool `json:"bluetooth"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) seccompArgs() FDBuilder {
 | 
					 | 
				
			||||||
	// explicitly disable syscall filter
 | 
					 | 
				
			||||||
	if c.Syscall == nil {
 | 
					 | 
				
			||||||
		// nil File skips builder
 | 
					 | 
				
			||||||
		return new(seccompBuilder)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		opts    seccomp.SyscallOpts
 | 
					 | 
				
			||||||
		optd    []string
 | 
					 | 
				
			||||||
		optCond = [...]struct {
 | 
					 | 
				
			||||||
			v bool
 | 
					 | 
				
			||||||
			o seccomp.SyscallOpts
 | 
					 | 
				
			||||||
			d string
 | 
					 | 
				
			||||||
		}{
 | 
					 | 
				
			||||||
			{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
 | 
					 | 
				
			||||||
			{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
 | 
					 | 
				
			||||||
			{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
 | 
					 | 
				
			||||||
			{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
 | 
					 | 
				
			||||||
			{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
 | 
					 | 
				
			||||||
			{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
 | 
					 | 
				
			||||||
			{c.Syscall.Can, seccomp.FlagCan, "can"},
 | 
					 | 
				
			||||||
			{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
		optd = make([]string, 1, len(optCond)+1)
 | 
					 | 
				
			||||||
		optd[0] = "common"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	for _, opt := range optCond {
 | 
					 | 
				
			||||||
		if opt.v {
 | 
					 | 
				
			||||||
			opts |= opt.o
 | 
					 | 
				
			||||||
			if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
				optd = append(optd, opt.d)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
		seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &seccompBuilder{seccomp.NewFile(opts)}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type seccompBuilder struct{ proc.File }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *seccompBuilder) Len() int {
 | 
					 | 
				
			||||||
	if s == nil || s.File == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *seccompBuilder) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if s == nil || s.File == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,273 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	gob.Register(new(PermConfig[SymlinkConfig]))
 | 
					 | 
				
			||||||
	gob.Register(new(PermConfig[*TmpfsConfig]))
 | 
					 | 
				
			||||||
	gob.Register(new(OverlayConfig))
 | 
					 | 
				
			||||||
	gob.Register(new(DataConfig))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PositionalArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p PositionalArg) String() string { return positionalArgs[p] }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	Tmpfs PositionalArg = iota
 | 
					 | 
				
			||||||
	Symlink
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Bind
 | 
					 | 
				
			||||||
	BindTry
 | 
					 | 
				
			||||||
	DevBind
 | 
					 | 
				
			||||||
	DevBindTry
 | 
					 | 
				
			||||||
	ROBind
 | 
					 | 
				
			||||||
	ROBindTry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Chmod
 | 
					 | 
				
			||||||
	Dir
 | 
					 | 
				
			||||||
	RemountRO
 | 
					 | 
				
			||||||
	Procfs
 | 
					 | 
				
			||||||
	DevTmpfs
 | 
					 | 
				
			||||||
	Mqueue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Perms
 | 
					 | 
				
			||||||
	Size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	OverlaySrc
 | 
					 | 
				
			||||||
	Overlay
 | 
					 | 
				
			||||||
	TmpOverlay
 | 
					 | 
				
			||||||
	ROOverlay
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	SyncFd
 | 
					 | 
				
			||||||
	Seccomp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	File
 | 
					 | 
				
			||||||
	BindData
 | 
					 | 
				
			||||||
	ROBindData
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var positionalArgs = [...]string{
 | 
					 | 
				
			||||||
	Tmpfs:   "--tmpfs",
 | 
					 | 
				
			||||||
	Symlink: "--symlink",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Bind:       "--bind",
 | 
					 | 
				
			||||||
	BindTry:    "--bind-try",
 | 
					 | 
				
			||||||
	DevBind:    "--dev-bind",
 | 
					 | 
				
			||||||
	DevBindTry: "--dev-bind-try",
 | 
					 | 
				
			||||||
	ROBind:     "--ro-bind",
 | 
					 | 
				
			||||||
	ROBindTry:  "--ro-bind-try",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Chmod:     "--chmod",
 | 
					 | 
				
			||||||
	Dir:       "--dir",
 | 
					 | 
				
			||||||
	RemountRO: "--remount-ro",
 | 
					 | 
				
			||||||
	Procfs:    "--proc",
 | 
					 | 
				
			||||||
	DevTmpfs:  "--dev",
 | 
					 | 
				
			||||||
	Mqueue:    "--mqueue",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Perms: "--perms",
 | 
					 | 
				
			||||||
	Size:  "--size",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	OverlaySrc: "--overlay-src",
 | 
					 | 
				
			||||||
	Overlay:    "--overlay",
 | 
					 | 
				
			||||||
	TmpOverlay: "--tmp-overlay",
 | 
					 | 
				
			||||||
	ROOverlay:  "--ro-overlay",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	SyncFd:  "--sync-fd",
 | 
					 | 
				
			||||||
	Seccomp: "--seccomp",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	File:       "--file",
 | 
					 | 
				
			||||||
	BindData:   "--bind-data",
 | 
					 | 
				
			||||||
	ROBindData: "--ro-bind-data",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PermConfig[T FSBuilder] struct {
 | 
					 | 
				
			||||||
	// set permissions of next argument
 | 
					 | 
				
			||||||
	// (--perms OCTAL)
 | 
					 | 
				
			||||||
	Mode *os.FileMode `json:"mode,omitempty"`
 | 
					 | 
				
			||||||
	// path to get the new permission
 | 
					 | 
				
			||||||
	// (--bind-data, --file, etc.)
 | 
					 | 
				
			||||||
	Inner T `json:"path"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Len() int {
 | 
					 | 
				
			||||||
	if p.Mode != nil {
 | 
					 | 
				
			||||||
		return p.Inner.Len() + 2
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return p.Inner.Len()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if p.Mode != nil {
 | 
					 | 
				
			||||||
		*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	p.Inner.Append(args)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type TmpfsConfig struct {
 | 
					 | 
				
			||||||
	// set size of tmpfs
 | 
					 | 
				
			||||||
	// (--size BYTES)
 | 
					 | 
				
			||||||
	Size int `json:"size,omitempty"`
 | 
					 | 
				
			||||||
	// mount point of new tmpfs
 | 
					 | 
				
			||||||
	// (--tmpfs DEST)
 | 
					 | 
				
			||||||
	Dir string `json:"dir"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Path() string { return t.Dir }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Len() int {
 | 
					 | 
				
			||||||
	if t.Size > 0 {
 | 
					 | 
				
			||||||
		return 4
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return 2
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if t.Size > 0 {
 | 
					 | 
				
			||||||
		*args = append(*args, Size.String(), strconv.Itoa(t.Size))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*args = append(*args, Tmpfs.String(), t.Dir)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type OverlayConfig struct {
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		read files from SRC in the following overlay
 | 
					 | 
				
			||||||
		(--overlay-src SRC)
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Src []string `json:"src,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		mount overlayfs on DEST, with RWSRC as the host path for writes and
 | 
					 | 
				
			||||||
		WORKDIR an empty directory on the same filesystem as RWSRC
 | 
					 | 
				
			||||||
		(--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
 | 
					 | 
				
			||||||
		(--tmp-overlay DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if either strings are empty, mount overlayfs read-only on DEST
 | 
					 | 
				
			||||||
		(--ro-overlay DEST)
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Persist *[2]string `json:"persist,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		--overlay RWSRC WORKDIR DEST
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		--tmp-overlay DEST
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		--ro-overlay DEST
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Dest string `json:"dest"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Path() string { return o.Dest }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Len() int {
 | 
					 | 
				
			||||||
	// (--tmp-overlay DEST) or (--ro-overlay DEST)
 | 
					 | 
				
			||||||
	p := 2
 | 
					 | 
				
			||||||
	// (--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
	if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
 | 
					 | 
				
			||||||
		p = 4
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return p + len(o.Src)*2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	// --overlay-src SRC
 | 
					 | 
				
			||||||
	for _, src := range o.Src {
 | 
					 | 
				
			||||||
		*args = append(*args, OverlaySrc.String(), src)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if o.Persist != nil {
 | 
					 | 
				
			||||||
		if o.Persist[0] != "" && o.Persist[1] != "" {
 | 
					 | 
				
			||||||
			// --overlay RWSRC WORKDIR
 | 
					 | 
				
			||||||
			*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// --ro-overlay
 | 
					 | 
				
			||||||
			*args = append(*args, ROOverlay.String())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// --tmp-overlay
 | 
					 | 
				
			||||||
		*args = append(*args, TmpOverlay.String())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// DEST
 | 
					 | 
				
			||||||
	*args = append(*args, o.Dest)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SymlinkConfig [2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Path() string          { return s[1] }
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Len() int              { return 3 }
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type ChmodConfig map[string]os.FileMode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c ChmodConfig) Len() int { return len(c) }
 | 
					 | 
				
			||||||
func (c ChmodConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for path, mode := range c {
 | 
					 | 
				
			||||||
		*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	DataWrite = iota
 | 
					 | 
				
			||||||
	DataBind
 | 
					 | 
				
			||||||
	DataROBind
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type DataConfig struct {
 | 
					 | 
				
			||||||
	Dest string `json:"dest"`
 | 
					 | 
				
			||||||
	Data []byte `json:"data,omitempty"`
 | 
					 | 
				
			||||||
	Type int    `json:"type"`
 | 
					 | 
				
			||||||
	proc.File
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (d *DataConfig) Path() string { return d.Dest }
 | 
					 | 
				
			||||||
func (d *DataConfig) Len() int {
 | 
					 | 
				
			||||||
	if d == nil || d.Data == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 3
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
 | 
					 | 
				
			||||||
	if d.File != nil {
 | 
					 | 
				
			||||||
		panic("file initialised twice")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	d.File = proc.NewWriterTo(d)
 | 
					 | 
				
			||||||
	return d.File.Init(fd, v)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
 | 
					 | 
				
			||||||
	n, err := w.Write(d.Data)
 | 
					 | 
				
			||||||
	return int64(n), err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if d == nil || d.Data == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	var a PositionalArg
 | 
					 | 
				
			||||||
	switch d.Type {
 | 
					 | 
				
			||||||
	case DataWrite:
 | 
					 | 
				
			||||||
		a = File
 | 
					 | 
				
			||||||
	case DataBind:
 | 
					 | 
				
			||||||
		a = BindData
 | 
					 | 
				
			||||||
	case DataROBind:
 | 
					 | 
				
			||||||
		a = ROBindData
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		panic(fmt.Sprintf("invalid type %d", a))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,249 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static boolean args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type BoolArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b BoolArg) Unwrap() []string {
 | 
					 | 
				
			||||||
	return boolArgs[b]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	UnshareAll BoolArg = iota
 | 
					 | 
				
			||||||
	UnshareUser
 | 
					 | 
				
			||||||
	UnshareIPC
 | 
					 | 
				
			||||||
	UnsharePID
 | 
					 | 
				
			||||||
	UnshareNet
 | 
					 | 
				
			||||||
	UnshareUTS
 | 
					 | 
				
			||||||
	UnshareCGroup
 | 
					 | 
				
			||||||
	ShareNet
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	UserNS
 | 
					 | 
				
			||||||
	Clearenv
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	NewSession
 | 
					 | 
				
			||||||
	DieWithParent
 | 
					 | 
				
			||||||
	AsInit
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var boolArgs = [...][]string{
 | 
					 | 
				
			||||||
	UnshareAll:    {"--unshare-all", "--unshare-user"},
 | 
					 | 
				
			||||||
	UnshareUser:   {"--unshare-user"},
 | 
					 | 
				
			||||||
	UnshareIPC:    {"--unshare-ipc"},
 | 
					 | 
				
			||||||
	UnsharePID:    {"--unshare-pid"},
 | 
					 | 
				
			||||||
	UnshareNet:    {"--unshare-net"},
 | 
					 | 
				
			||||||
	UnshareUTS:    {"--unshare-uts"},
 | 
					 | 
				
			||||||
	UnshareCGroup: {"--unshare-cgroup"},
 | 
					 | 
				
			||||||
	ShareNet:      {"--share-net"},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	UserNS:   {"--disable-userns", "--assert-userns-disabled"},
 | 
					 | 
				
			||||||
	Clearenv: {"--clearenv"},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	NewSession:    {"--new-session"},
 | 
					 | 
				
			||||||
	DieWithParent: {"--die-with-parent"},
 | 
					 | 
				
			||||||
	AsInit:        {"--as-pid-1"},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) boolArgs() Builder {
 | 
					 | 
				
			||||||
	b := boolArg{
 | 
					 | 
				
			||||||
		UserNS:   !c.UserNS,
 | 
					 | 
				
			||||||
		Clearenv: c.Clearenv,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		NewSession:    c.NewSession,
 | 
					 | 
				
			||||||
		DieWithParent: c.DieWithParent,
 | 
					 | 
				
			||||||
		AsInit:        c.AsInit,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if c.Unshare == nil {
 | 
					 | 
				
			||||||
		b[UnshareAll] = true
 | 
					 | 
				
			||||||
		b[ShareNet] = c.Net
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		b[UnshareUser] = c.Unshare.User
 | 
					 | 
				
			||||||
		b[UnshareIPC] = c.Unshare.IPC
 | 
					 | 
				
			||||||
		b[UnsharePID] = c.Unshare.PID
 | 
					 | 
				
			||||||
		b[UnshareNet] = c.Unshare.Net
 | 
					 | 
				
			||||||
		b[UnshareUTS] = c.Unshare.UTS
 | 
					 | 
				
			||||||
		b[UnshareCGroup] = c.Unshare.CGroup
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &b
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type boolArg [len(boolArgs)]bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *boolArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for i, v := range b {
 | 
					 | 
				
			||||||
		if v {
 | 
					 | 
				
			||||||
			l += len(boolArgs[i])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *boolArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, v := range b {
 | 
					 | 
				
			||||||
		if v {
 | 
					 | 
				
			||||||
			*args = append(*args, BoolArg(i).Unwrap()...)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static integer args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type IntArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (i IntArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return intArgs[i]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	UID IntArg = iota
 | 
					 | 
				
			||||||
	GID
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var intArgs = [...]string{
 | 
					 | 
				
			||||||
	UID: "--uid",
 | 
					 | 
				
			||||||
	GID: "--gid",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) intArgs() Builder {
 | 
					 | 
				
			||||||
	return &intArg{
 | 
					 | 
				
			||||||
		UID: c.UID,
 | 
					 | 
				
			||||||
		GID: c.GID,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type intArg [len(intArgs)]*int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *intArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, v := range n {
 | 
					 | 
				
			||||||
		if v != nil {
 | 
					 | 
				
			||||||
			l += 2
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *intArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, v := range n {
 | 
					 | 
				
			||||||
		if v != nil {
 | 
					 | 
				
			||||||
			*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static string args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type StringArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s StringArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return stringArgs[s]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	Hostname StringArg = iota
 | 
					 | 
				
			||||||
	Chdir
 | 
					 | 
				
			||||||
	UnsetEnv
 | 
					 | 
				
			||||||
	LockFile
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var stringArgs = [...]string{
 | 
					 | 
				
			||||||
	Hostname: "--hostname",
 | 
					 | 
				
			||||||
	Chdir:    "--chdir",
 | 
					 | 
				
			||||||
	UnsetEnv: "--unsetenv",
 | 
					 | 
				
			||||||
	LockFile: "--lock-file",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) stringArgs() Builder {
 | 
					 | 
				
			||||||
	n := stringArg{
 | 
					 | 
				
			||||||
		UnsetEnv: c.UnsetEnv,
 | 
					 | 
				
			||||||
		LockFile: c.LockFile,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if c.Hostname != "" {
 | 
					 | 
				
			||||||
		n[Hostname] = []string{c.Hostname}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if c.Chdir != "" {
 | 
					 | 
				
			||||||
		n[Chdir] = []string{c.Chdir}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type stringArg [len(stringArgs)][]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *stringArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, arg := range s {
 | 
					 | 
				
			||||||
		l += len(arg) * 2
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *stringArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, arg := range s {
 | 
					 | 
				
			||||||
		for _, v := range arg {
 | 
					 | 
				
			||||||
			*args = append(*args, StringArg(i).Unwrap(), v)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static pair args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PairArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p PairArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return pairArgs[p]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	SetEnv PairArg = iota
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var pairArgs = [...]string{
 | 
					 | 
				
			||||||
	SetEnv: "--setenv",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) pairArgs() Builder {
 | 
					 | 
				
			||||||
	var n pairArg
 | 
					 | 
				
			||||||
	n[SetEnv] = make([][2]string, len(c.SetEnv))
 | 
					 | 
				
			||||||
	keys := make([]string, 0, len(c.SetEnv))
 | 
					 | 
				
			||||||
	for k := range c.SetEnv {
 | 
					 | 
				
			||||||
		keys = append(keys, k)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	slices.Sort(keys)
 | 
					 | 
				
			||||||
	for i, k := range keys {
 | 
					 | 
				
			||||||
		n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type pairArg [len(pairArgs)][][2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, v := range p {
 | 
					 | 
				
			||||||
		l += len(v) * 3
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, arg := range p {
 | 
					 | 
				
			||||||
		for _, v := range arg {
 | 
					 | 
				
			||||||
			*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,52 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	gob.Register(new(pairF))
 | 
					 | 
				
			||||||
	gob.Register(new(stringF))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type pairF [3]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairF) Path() string          { return p[2] }
 | 
					 | 
				
			||||||
func (p *pairF) Len() int              { return len(p) }
 | 
					 | 
				
			||||||
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type stringF [2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s stringF) Path() string          { return s[1] }
 | 
					 | 
				
			||||||
func (s stringF) Len() int              { return len(s) /* compiler replaces this with 2 */ }
 | 
					 | 
				
			||||||
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type fileF struct {
 | 
					 | 
				
			||||||
	name string
 | 
					 | 
				
			||||||
	file *os.File
 | 
					 | 
				
			||||||
	proc.BaseFile
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) ErrCount() int                                  { return 0 }
 | 
					 | 
				
			||||||
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) Len() int {
 | 
					 | 
				
			||||||
	if f.file == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if f.file == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,103 +0,0 @@
 | 
				
			|||||||
package helper_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestBwrap(t *testing.T) {
 | 
					 | 
				
			||||||
	sc := &bwrap.Config{
 | 
					 | 
				
			||||||
		Net:           true,
 | 
					 | 
				
			||||||
		Hostname:      "localhost",
 | 
					 | 
				
			||||||
		Chdir:         "/nonexistent",
 | 
					 | 
				
			||||||
		Clearenv:      true,
 | 
					 | 
				
			||||||
		NewSession:    true,
 | 
					 | 
				
			||||||
		DieWithParent: true,
 | 
					 | 
				
			||||||
		AsInit:        true,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("nonexistent bwrap name", func(t *testing.T) {
 | 
					 | 
				
			||||||
		bubblewrapName := helper.BubblewrapName
 | 
					 | 
				
			||||||
		helper.BubblewrapName = "/nonexistent"
 | 
					 | 
				
			||||||
		t.Cleanup(func() {
 | 
					 | 
				
			||||||
			helper.BubblewrapName = bubblewrapName
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "fortify",
 | 
					 | 
				
			||||||
			argsWt, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v, wantErr %v",
 | 
					 | 
				
			||||||
				err, os.ErrNotExist)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
					 | 
				
			||||||
		if got := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "fortify",
 | 
					 | 
				
			||||||
			argsWt, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		); got == nil {
 | 
					 | 
				
			||||||
			t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
 | 
					 | 
				
			||||||
				sc, argsWt, "fortify")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer func() {
 | 
					 | 
				
			||||||
			wantPanic := "argument contains null character"
 | 
					 | 
				
			||||||
			if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
				t.Errorf("MustNewBwrap: panic = %q, want %q",
 | 
					 | 
				
			||||||
					r, wantPanic)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			&bwrap.Config{Hostname: "\x00"}, "fortify",
 | 
					 | 
				
			||||||
			nil, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("start without pipes", func(t *testing.T) {
 | 
					 | 
				
			||||||
		helper.InternalReplaceExecCommand(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "crash-test-dummy",
 | 
					 | 
				
			||||||
			nil, argFChecked,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
					 | 
				
			||||||
		defer cancel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(c, false); err != nil {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v",
 | 
					 | 
				
			||||||
				err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Wait(); err != nil {
 | 
					 | 
				
			||||||
			t.Errorf("Wait() err = %v stderr = %s",
 | 
					 | 
				
			||||||
				err, stderr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
					 | 
				
			||||||
		testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										84
									
								
								helper/cmd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								helper/cmd.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
 | 
				
			||||||
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
 | 
					func NewDirect(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						cmdF func(cmd *exec.Cmd),
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) Helper {
 | 
				
			||||||
 | 
						d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						d.Args = append(d.Args, args...)
 | 
				
			||||||
 | 
						if cmdF != nil {
 | 
				
			||||||
 | 
							cmdF(d.Cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return d
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newHelperCmd(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) (cmd *helperCmd, args []string) {
 | 
				
			||||||
 | 
						cmd = new(helperCmd)
 | 
				
			||||||
 | 
						cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						cmd.Cmd = exec.CommandContext(ctx, name)
 | 
				
			||||||
 | 
						cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
 | 
				
			||||||
 | 
						cmd.WaitDelay = WaitDelay
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
 | 
				
			||||||
 | 
					type helperCmd struct {
 | 
				
			||||||
 | 
						mu sync.RWMutex
 | 
				
			||||||
 | 
						*helperFiles
 | 
				
			||||||
 | 
						*exec.Cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *helperCmd) Start() error {
 | 
				
			||||||
 | 
						h.mu.Lock()
 | 
				
			||||||
 | 
						defer h.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
				
			||||||
 | 
						// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
				
			||||||
 | 
						if h.Cmd != nil && h.Cmd.Process != nil {
 | 
				
			||||||
 | 
							return errors.New("helper: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h.Env = slices.Grow(h.Env, 2)
 | 
				
			||||||
 | 
						if h.useArgsFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=1")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if h.useStatFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stat is populated on fulfill
 | 
				
			||||||
 | 
							h.Cancel = func() error { return h.stat.Close() }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								helper/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								helper/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package helper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCmd(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("start non-existent helper path", func(t *testing.T) {
 | 
				
			||||||
 | 
							h := helper.NewDirect(context.Background(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
								t.Errorf("Start: error = %v, wantErr %v",
 | 
				
			||||||
 | 
									err, os.ErrNotExist)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
 | 
							if got := helper.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
 | 
				
			||||||
 | 
								t.Errorf("NewDirect(%q, %q) got nil",
 | 
				
			||||||
 | 
									argsWt, "fortify")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
 | 
							testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
 | 
				
			||||||
 | 
								return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
 | 
				
			||||||
 | 
									setOutput(&cmd.Stdout, &cmd.Stderr)
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										76
									
								
								helper/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								helper/container.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New initialises a Helper instance with wt as the null-terminated argument writer.
 | 
				
			||||||
 | 
					func New(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						cmdF func(container *sandbox.Container),
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) Helper {
 | 
				
			||||||
 | 
						var args []string
 | 
				
			||||||
 | 
						h := new(helperContainer)
 | 
				
			||||||
 | 
						h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						h.Container = sandbox.New(ctx, name, args...)
 | 
				
			||||||
 | 
						h.WaitDelay = WaitDelay
 | 
				
			||||||
 | 
						if cmdF != nil {
 | 
				
			||||||
 | 
							cmdF(h.Container)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
 | 
				
			||||||
 | 
					type helperContainer struct {
 | 
				
			||||||
 | 
						started bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mu sync.Mutex
 | 
				
			||||||
 | 
						*helperFiles
 | 
				
			||||||
 | 
						*sandbox.Container
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *helperContainer) Start() error {
 | 
				
			||||||
 | 
						h.mu.Lock()
 | 
				
			||||||
 | 
						defer h.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if h.started {
 | 
				
			||||||
 | 
							return errors.New("helper: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.started = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h.Env = slices.Grow(h.Env, 2)
 | 
				
			||||||
 | 
						if h.useArgsFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=1")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if h.useStatFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stat is populated on fulfill
 | 
				
			||||||
 | 
							h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
 | 
				
			||||||
 | 
							if err := h.Container.Start(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return h.Container.Serve()
 | 
				
			||||||
 | 
						}, h.files, h.extraFiles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								helper/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								helper/container_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					package helper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainer(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("start empty container", func(t *testing.T) {
 | 
				
			||||||
 | 
							h := helper.New(context.Background(), "/nonexistent", argsWt, false, argF, nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							wantErr := "sandbox: starting an empty container"
 | 
				
			||||||
 | 
							if err := h.Start(); err == nil || err.Error() != wantErr {
 | 
				
			||||||
 | 
								t.Errorf("Start: error = %v, wantErr %q",
 | 
				
			||||||
 | 
									err, wantErr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
 | 
							if got := helper.New(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
 | 
				
			||||||
 | 
								t.Errorf("New(%q, %q) got nil",
 | 
				
			||||||
 | 
									argsWt, "fortify")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
 | 
							testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
 | 
				
			||||||
 | 
								return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *sandbox.Container) {
 | 
				
			||||||
 | 
									setOutput(&container.Stdout, &container.Stderr)
 | 
				
			||||||
 | 
									container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
 | 
				
			||||||
 | 
										return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
											"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									container.Bind("/", "/", 0)
 | 
				
			||||||
 | 
									container.Proc("/proc")
 | 
				
			||||||
 | 
									container.Dev("/dev")
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						sandbox.Init(fmsg.Prepare, func(bool) { internal.InstallFmsg(false) })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,40 +0,0 @@
 | 
				
			|||||||
package helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// direct wraps *exec.Cmd and manages status and args fd.
 | 
					 | 
				
			||||||
// Args is always 3 and status if set is always 4.
 | 
					 | 
				
			||||||
type direct struct {
 | 
					 | 
				
			||||||
	lock sync.RWMutex
 | 
					 | 
				
			||||||
	*helperCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (h *direct) Start(ctx context.Context, stat bool) error {
 | 
					 | 
				
			||||||
	h.lock.Lock()
 | 
					 | 
				
			||||||
	defer h.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
					 | 
				
			||||||
	// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
					 | 
				
			||||||
	if h.Cmd != nil && h.Cmd.Process != nil {
 | 
					 | 
				
			||||||
		return errors.New("exec: already started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args := h.finalise(ctx, stat)
 | 
					 | 
				
			||||||
	h.Cmd.Args = append(h.Cmd.Args, args...)
 | 
					 | 
				
			||||||
	return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New initialises a new direct Helper instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					 | 
				
			||||||
func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
 | 
					 | 
				
			||||||
	d := new(direct)
 | 
					 | 
				
			||||||
	d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
 | 
					 | 
				
			||||||
	return d
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,33 +0,0 @@
 | 
				
			|||||||
package helper_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestDirect(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("start non-existent helper path", func(t *testing.T) {
 | 
					 | 
				
			||||||
		h := helper.New(argsWt, "/nonexistent", argF)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v, wantErr %v",
 | 
					 | 
				
			||||||
				err, os.ErrNotExist)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
					 | 
				
			||||||
		if got := helper.New(argsWt, "fortify", argF); got == nil {
 | 
					 | 
				
			||||||
			t.Errorf("New(%q, %q) got nil",
 | 
					 | 
				
			||||||
				argsWt, "fortify")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
					 | 
				
			||||||
		testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										117
									
								
								helper/helper.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								helper/helper.go
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
				
			|||||||
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
 | 
					// Package helper runs external helpers with optional sandboxing.
 | 
				
			||||||
package helper
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
@ -6,17 +6,12 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var WaitDelay = 2 * time.Second
 | 
				
			||||||
	WaitDelay = 2 * time.Second
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
 | 
						// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
 | 
				
			||||||
@ -26,62 +21,56 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Helper interface {
 | 
					type Helper interface {
 | 
				
			||||||
	// Stdin sets the standard input of Helper.
 | 
					 | 
				
			||||||
	Stdin(r io.Reader) Helper
 | 
					 | 
				
			||||||
	// Stdout sets the standard output of Helper.
 | 
					 | 
				
			||||||
	Stdout(w io.Writer) Helper
 | 
					 | 
				
			||||||
	// Stderr sets the standard error of Helper.
 | 
					 | 
				
			||||||
	Stderr(w io.Writer) Helper
 | 
					 | 
				
			||||||
	// SetEnv sets the environment of Helper.
 | 
					 | 
				
			||||||
	SetEnv(env []string) Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Start starts the helper process.
 | 
						// Start starts the helper process.
 | 
				
			||||||
	// A status pipe is passed to the helper if stat is true.
 | 
						Start() error
 | 
				
			||||||
	Start(ctx context.Context, stat bool) error
 | 
						// Wait blocks until Helper exits.
 | 
				
			||||||
	// Wait blocks until Helper exits and releases all its resources.
 | 
					 | 
				
			||||||
	Wait() error
 | 
						Wait() error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Stringer
 | 
						fmt.Stringer
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newHelperCmd(
 | 
					func newHelperFiles(
 | 
				
			||||||
	h Helper, name string,
 | 
						ctx context.Context,
 | 
				
			||||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
	extraFiles []*os.File,
 | 
						extraFiles []*os.File,
 | 
				
			||||||
) (cmd *helperCmd) {
 | 
					) (hl *helperFiles, args []string) {
 | 
				
			||||||
	cmd = new(helperCmd)
 | 
						hl = new(helperFiles)
 | 
				
			||||||
 | 
						hl.ctx = ctx
 | 
				
			||||||
 | 
						hl.useArgsFd = wt != nil
 | 
				
			||||||
 | 
						hl.useStatFd = stat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cmd.r = h
 | 
						hl.extraFiles = new(proc.ExtraFilesPre)
 | 
				
			||||||
	cmd.name = name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cmd.extraFiles = new(proc.ExtraFilesPre)
 | 
					 | 
				
			||||||
	for _, f := range extraFiles {
 | 
						for _, f := range extraFiles {
 | 
				
			||||||
		_, v := cmd.extraFiles.Append()
 | 
							_, v := hl.extraFiles.Append()
 | 
				
			||||||
		*v = f
 | 
							*v = f
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	argsFd := -1
 | 
						argsFd := -1
 | 
				
			||||||
	if wt != nil {
 | 
						if hl.useArgsFd {
 | 
				
			||||||
		f := proc.NewWriterTo(wt)
 | 
							f := proc.NewWriterTo(wt)
 | 
				
			||||||
		argsFd = int(proc.InitFile(f, cmd.extraFiles))
 | 
							argsFd = int(proc.InitFile(f, hl.extraFiles))
 | 
				
			||||||
		cmd.files = append(cmd.files, f)
 | 
							hl.files = append(hl.files, f)
 | 
				
			||||||
		cmd.hasArgsFd = true
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						statFd := -1
 | 
				
			||||||
 | 
						if hl.useStatFd {
 | 
				
			||||||
 | 
							f := proc.NewStat(&hl.stat)
 | 
				
			||||||
 | 
							statFd = int(proc.InitFile(f, hl.extraFiles))
 | 
				
			||||||
 | 
							hl.files = append(hl.files, f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args = argF(argsFd, statFd)
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
 | 
					// helperFiles provides a generic wrapper around helper ipc.
 | 
				
			||||||
type helperCmd struct {
 | 
					type helperFiles struct {
 | 
				
			||||||
	// ref to parent
 | 
					 | 
				
			||||||
	r Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// returns an array of arguments passed directly
 | 
					 | 
				
			||||||
	// to the helper process
 | 
					 | 
				
			||||||
	argF func(statFd int) []string
 | 
					 | 
				
			||||||
	// whether argsFd is present
 | 
						// whether argsFd is present
 | 
				
			||||||
	hasArgsFd bool
 | 
						useArgsFd bool
 | 
				
			||||||
 | 
						// whether statFd is present
 | 
				
			||||||
 | 
						useStatFd bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// closes statFd
 | 
						// closes statFd
 | 
				
			||||||
	stat io.Closer
 | 
						stat io.Closer
 | 
				
			||||||
@ -90,45 +79,5 @@ type helperCmd struct {
 | 
				
			|||||||
	// passed through to [proc.Fulfill] and [proc.InitFile]
 | 
						// passed through to [proc.Fulfill] and [proc.InitFile]
 | 
				
			||||||
	extraFiles *proc.ExtraFilesPre
 | 
						extraFiles *proc.ExtraFilesPre
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	name           string
 | 
						ctx context.Context
 | 
				
			||||||
	stdin          io.Reader
 | 
					 | 
				
			||||||
	stdout, stderr io.Writer
 | 
					 | 
				
			||||||
	env            []string
 | 
					 | 
				
			||||||
	*exec.Cmd
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func (h *helperCmd) Stdin(r io.Reader) Helper   { h.stdin = r; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) Stdout(w io.Writer) Helper  { h.stdout = w; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) Stderr(w io.Writer) Helper  { h.stderr = w; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) SetEnv(env []string) Helper { h.env = env; return h.r }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// finalise initialises the underlying [exec.Cmd] object.
 | 
					 | 
				
			||||||
func (h *helperCmd) finalise(ctx context.Context, stat bool) (args []string) {
 | 
					 | 
				
			||||||
	h.Cmd = commandContext(ctx, h.name)
 | 
					 | 
				
			||||||
	h.Cmd.Stdin, h.Cmd.Stdout, h.Cmd.Stderr = h.stdin, h.stdout, h.stderr
 | 
					 | 
				
			||||||
	h.Cmd.Env = slices.Grow(h.env, 2)
 | 
					 | 
				
			||||||
	if h.hasArgsFd {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=0")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	h.Cmd.Cancel = func() error { return h.Cmd.Process.Signal(syscall.SIGTERM) }
 | 
					 | 
				
			||||||
	h.Cmd.WaitDelay = WaitDelay
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	statFd := -1
 | 
					 | 
				
			||||||
	if stat {
 | 
					 | 
				
			||||||
		f := proc.NewStat(&h.stat)
 | 
					 | 
				
			||||||
		statFd = int(proc.InitFile(f, h.extraFiles))
 | 
					 | 
				
			||||||
		h.files = append(h.files, f)
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=1")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// stat is populated on fulfill
 | 
					 | 
				
			||||||
		h.Cmd.Cancel = func() error { return h.stat.Close() }
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=0")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return h.argF(statFd)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var commandContext = exec.CommandContext
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
@ -35,7 +36,8 @@ func argF(argsFd, statFd int) []string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func argFChecked(argsFd, statFd int) (args []string) {
 | 
					func argFChecked(argsFd, statFd int) (args []string) {
 | 
				
			||||||
	args = make([]string, 0, 4)
 | 
						args = make([]string, 0, 6)
 | 
				
			||||||
 | 
						args = append(args, "-test.run=TestHelperStub", "--")
 | 
				
			||||||
	if argsFd > -1 {
 | 
						if argsFd > -1 {
 | 
				
			||||||
		args = append(args, "--args", strconv.Itoa(argsFd))
 | 
							args = append(args, "--args", strconv.Itoa(argsFd))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -46,14 +48,15 @@ func argFChecked(argsFd, statFd int) (args []string) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this function tests an implementation of the helper.Helper interface
 | 
					// this function tests an implementation of the helper.Helper interface
 | 
				
			||||||
func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
					func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
 | 
				
			||||||
	helper.InternalReplaceExecCommand(t)
 | 
						oldWaitDelay := helper.WaitDelay
 | 
				
			||||||
 | 
						helper.WaitDelay = 16 * time.Second
 | 
				
			||||||
 | 
						t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start helper with status channel and wait", func(t *testing.T) {
 | 
						t.Run("start helper with status channel and wait", func(t *testing.T) {
 | 
				
			||||||
		h := createHelper()
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
							stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
							h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("wait not yet started helper", func(t *testing.T) {
 | 
							t.Run("wait not yet started helper", func(t *testing.T) {
 | 
				
			||||||
			defer func() {
 | 
								defer func() {
 | 
				
			||||||
@ -65,10 +68,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
			panic(fmt.Sprintf("unreachable: %v", h.Wait()))
 | 
								panic(fmt.Sprintf("unreachable: %v", h.Wait()))
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		t.Log("starting helper stub")
 | 
							t.Log("starting helper stub")
 | 
				
			||||||
		if err := h.Start(ctx, true); err != nil {
 | 
							if err := h.Start(); err != nil {
 | 
				
			||||||
			t.Errorf("Start: error = %v", err)
 | 
								t.Errorf("Start: error = %v", err)
 | 
				
			||||||
			cancel()
 | 
								cancel()
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@ -77,8 +78,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
		cancel()
 | 
							cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("start already started helper", func(t *testing.T) {
 | 
							t.Run("start already started helper", func(t *testing.T) {
 | 
				
			||||||
			wantErr := "exec: already started"
 | 
								wantErr := "helper: already started"
 | 
				
			||||||
			if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
 | 
								if err := h.Start(); err != nil && err.Error() != wantErr {
 | 
				
			||||||
				t.Errorf("Start: error = %v, wantErr %v",
 | 
									t.Errorf("Start: error = %v, wantErr %v",
 | 
				
			||||||
					err, wantErr)
 | 
										err, wantErr)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -100,21 +101,19 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
 | 
							if got := stderr.String(); got != wantPayload {
 | 
				
			||||||
			t.Errorf("Start: stdout = %v, want %v",
 | 
								t.Errorf("Start: stderr = %v, want %v",
 | 
				
			||||||
				got, wantPayload)
 | 
									got, wantPayload)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start helper and wait", func(t *testing.T) {
 | 
						t.Run("start helper and wait", func(t *testing.T) {
 | 
				
			||||||
		h := createHelper()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
		defer cancel()
 | 
							defer cancel()
 | 
				
			||||||
 | 
							stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
				
			||||||
 | 
							h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := h.Start(ctx, false); err != nil {
 | 
							if err := h.Start(); err != nil {
 | 
				
			||||||
			t.Errorf("Start() error = %v",
 | 
								t.Errorf("Start() error = %v",
 | 
				
			||||||
				err)
 | 
									err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@ -125,8 +124,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
				err, stdout, stderr)
 | 
									err, stdout, stderr)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
 | 
							if got := stderr.String(); got != wantPayload {
 | 
				
			||||||
			t.Errorf("Start() stdout = %v, want %v",
 | 
								t.Errorf("Start() stderr = %v, want %v",
 | 
				
			||||||
				got, wantPayload)
 | 
									got, wantPayload)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
				
			|||||||
@ -60,7 +60,10 @@ func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
 | 
					// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
 | 
				
			||||||
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
 | 
					func Fulfill(ctx context.Context,
 | 
				
			||||||
 | 
						v *[]*os.File, start func() error,
 | 
				
			||||||
 | 
						files []File, extraFiles *ExtraFilesPre,
 | 
				
			||||||
 | 
					) (err error) {
 | 
				
			||||||
	var ecs int
 | 
						var ecs int
 | 
				
			||||||
	for _, o := range files {
 | 
						for _, o := range files {
 | 
				
			||||||
		ecs += o.ErrCount()
 | 
							ecs += o.ErrCount()
 | 
				
			||||||
@ -77,8 +80,8 @@ func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *Extra
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cmd.ExtraFiles = extraFiles.Files()
 | 
						*v = extraFiles.Files()
 | 
				
			||||||
	if err = cmd.Start(); err != nil {
 | 
						if err = start(); err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,17 @@
 | 
				
			|||||||
package helper
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"flag"
 | 
						"flag"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// InternalChildStub is an internal function but exported because it is cross-package;
 | 
					// InternalHelperStub is an internal function but exported because it is cross-package;
 | 
				
			||||||
// it is part of the implementation of the helper stub.
 | 
					// it is part of the implementation of the helper stub.
 | 
				
			||||||
func InternalChildStub() {
 | 
					func InternalHelperStub() {
 | 
				
			||||||
	// this test mocks the helper process
 | 
						// this test mocks the helper process
 | 
				
			||||||
	var ap, sp string
 | 
						var ap, sp string
 | 
				
			||||||
	if v, ok := os.LookupEnv(FortifyHelper); !ok {
 | 
						if v, ok := os.LookupEnv(FortifyHelper); !ok {
 | 
				
			||||||
@ -33,30 +25,9 @@ func InternalChildStub() {
 | 
				
			|||||||
		sp = v
 | 
							sp = v
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch os.Args[3] {
 | 
						genericStub(flagRestoreFiles(3, ap, sp))
 | 
				
			||||||
	case "bwrap":
 | 
					 | 
				
			||||||
		bwrapStub()
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		genericStub(flagRestoreFiles(4, ap, sp))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal.Exit(0)
 | 
						os.Exit(0)
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
 | 
					 | 
				
			||||||
// it is part of the implementation of the helper stub.
 | 
					 | 
				
			||||||
func InternalReplaceExecCommand(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Cleanup(func() { commandContext = exec.CommandContext })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
 | 
					 | 
				
			||||||
	commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
 | 
					 | 
				
			||||||
		// pass through nonexistent path
 | 
					 | 
				
			||||||
		if name == "/nonexistent" && len(arg) == 0 {
 | 
					 | 
				
			||||||
			return exec.CommandContext(ctx, name)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newFile(fd int, name, p string) *os.File {
 | 
					func newFile(fd int, name, p string) *os.File {
 | 
				
			||||||
@ -92,7 +63,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
 | 
				
			|||||||
func genericStub(argsFile, statFile *os.File) {
 | 
					func genericStub(argsFile, statFile *os.File) {
 | 
				
			||||||
	if argsFile != nil {
 | 
						if argsFile != nil {
 | 
				
			||||||
		// this output is checked by parent
 | 
							// this output is checked by parent
 | 
				
			||||||
		if _, err := io.Copy(os.Stdout, argsFile); err != nil {
 | 
							if _, err := io.Copy(os.Stderr, argsFile); err != nil {
 | 
				
			||||||
			panic("cannot read args: " + err.Error())
 | 
								panic("cannot read args: " + err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -133,42 +104,3 @@ func genericStub(argsFile, statFile *os.File) {
 | 
				
			|||||||
		<-done
 | 
							<-done
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func bwrapStub() {
 | 
					 | 
				
			||||||
	// the bwrap launcher does not launch with a typical sync fd
 | 
					 | 
				
			||||||
	argsFile, _ := flagRestoreFiles(4, "1", "0")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// test args pipe behaviour
 | 
					 | 
				
			||||||
	func() {
 | 
					 | 
				
			||||||
		got, want := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		if _, err := io.Copy(got, argsFile); err != nil {
 | 
					 | 
				
			||||||
			panic("cannot read bwrap args: " + err.Error())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// hardcoded bwrap configuration used by test
 | 
					 | 
				
			||||||
		sc := &bwrap.Config{
 | 
					 | 
				
			||||||
			Net:           true,
 | 
					 | 
				
			||||||
			Hostname:      "localhost",
 | 
					 | 
				
			||||||
			Chdir:         "/nonexistent",
 | 
					 | 
				
			||||||
			Clearenv:      true,
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
			AsInit:        true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
 | 
					 | 
				
			||||||
			WriteTo(want); err != nil {
 | 
					 | 
				
			||||||
			panic("cannot read want: " + err.Error())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
 | 
					 | 
				
			||||||
			panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := syscall.Exec(
 | 
					 | 
				
			||||||
		os.Args[0],
 | 
					 | 
				
			||||||
		append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
 | 
					 | 
				
			||||||
		os.Environ()); err != nil {
 | 
					 | 
				
			||||||
		panic("cannot start general stub: " + err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,4 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestHelperChildStub(t *testing.T) {
 | 
					func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
 | 
				
			||||||
	helper.InternalChildStub()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
@ -10,9 +11,10 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func New(os sys.State) (fst.App, error) {
 | 
					func New(ctx context.Context, os sys.State) (fst.App, error) {
 | 
				
			||||||
	a := new(app)
 | 
						a := new(app)
 | 
				
			||||||
	a.sys = os
 | 
						a.sys = os
 | 
				
			||||||
 | 
						a.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	id := new(fst.ID)
 | 
						id := new(fst.ID)
 | 
				
			||||||
	err := fst.NewAppID(id)
 | 
						err := fst.NewAppID(id)
 | 
				
			||||||
@ -21,8 +23,8 @@ func New(os sys.State) (fst.App, error) {
 | 
				
			|||||||
	return a, err
 | 
						return a, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustNew(os sys.State) fst.App {
 | 
					func MustNew(ctx context.Context, os sys.State) fst.App {
 | 
				
			||||||
	a, err := New(os)
 | 
						a, err := New(ctx, os)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatalf("cannot create app: %v", err)
 | 
							log.Fatalf("cannot create app: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -32,6 +34,7 @@ func MustNew(os sys.State) fst.App {
 | 
				
			|||||||
type app struct {
 | 
					type app struct {
 | 
				
			||||||
	id  *stringPair[fst.ID]
 | 
						id  *stringPair[fst.ID]
 | 
				
			||||||
	sys sys.State
 | 
						sys sys.State
 | 
				
			||||||
 | 
						ctx context.Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	*outcome
 | 
						*outcome
 | 
				
			||||||
	mu sync.RWMutex
 | 
						mu sync.RWMutex
 | 
				
			||||||
@ -71,7 +74,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	seal := new(outcome)
 | 
						seal := new(outcome)
 | 
				
			||||||
	seal.id = a.id
 | 
						seal.id = a.id
 | 
				
			||||||
	err := seal.finalise(a.sys, config)
 | 
						err := seal.finalise(a.ctx, a.sys, config)
 | 
				
			||||||
	if err == nil {
 | 
						if err == nil {
 | 
				
			||||||
		a.outcome = seal
 | 
							a.outcome = seal
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,20 +12,20 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos chromium direct wayland", new(stubNixOS),
 | 
							"nixos chromium direct wayland", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
								Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
									AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
				
			||||||
				Outer: "/var/lib/persist/module/fortify/0/1",
 | 
									Outer: "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				Sandbox: &fst.SandboxConfig{
 | 
									Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
					UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
										Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
				
			||||||
					Filesystem: []*fst.FilesystemConfig{
 | 
										Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
						{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
											{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
				
			||||||
						{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
											{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
				
			||||||
						{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
											{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
				
			||||||
						{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
											{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					Override: []string{"/var/run/nscd"},
 | 
										Cover: []string{"/var/run/nscd"},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				SystemBus: &dbus.Config{
 | 
									SystemBus: &dbus.Config{
 | 
				
			||||||
					Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
										Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
				
			||||||
@ -45,7 +45,7 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
					Call: map[string]string{}, Broadcast: map[string]string{},
 | 
										Call: map[string]string{}, Broadcast: map[string]string{},
 | 
				
			||||||
					Filter: true,
 | 
										Filter: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
									Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		fst.ID{
 | 
							fst.ID{
 | 
				
			||||||
@ -88,136 +88,132 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Uid:   1971,
 | 
				
			||||||
			UserNS:   true,
 | 
								Gid:   100,
 | 
				
			||||||
			Chdir:    "/var/lib/persist/module/fortify/0/1",
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
 | 
				
			||||||
			Clearenv: true,
 | 
								Dir:   "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
								Path:  "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
 | 
								Args:  []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
								Env: []string{
 | 
				
			||||||
				"HOME":                     "/var/lib/persist/module/fortify/0/1",
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/1971/pulse/native",
 | 
									"HOME=/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"PULSE_SERVER=unix:/run/user/1971/pulse/native",
 | 
				
			||||||
				"USER":                     "u0_a1",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"USER=u0_a1",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/1971",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
									"XDG_RUNTIME_DIR=/run/user/1971",
 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			NewSession:    true,
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(1971).SetGID(1971).
 | 
									Bind("/bin", "/bin", 0).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/usr/bin", "/usr/bin", 0).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/nix/store", "/nix/store", 0).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/run/current-system", "/run/current-system", 0).
 | 
				
			||||||
			Bind("/bin", "/bin").
 | 
									Bind("/sys/block", "/sys/block", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr/bin", "/usr/bin").
 | 
									Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/nix/store", "/nix/store").
 | 
									Bind("/sys/class", "/sys/class", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/run/current-system", "/run/current-system").
 | 
									Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/block", "/sys/block", true).
 | 
									Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/bus", "/sys/bus", true).
 | 
									Bind("/run/opengl-driver", "/run/opengl-driver", 0).
 | 
				
			||||||
			Bind("/sys/class", "/sys/class", true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/dev", "/sys/dev", true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/sys/devices", "/sys/devices", true).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Bind("/run/opengl-driver", "/run/opengl-driver").
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user/1971", 8388608, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8388608).
 | 
									Place("/etc/group", []byte("fortify:x:100:\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
 | 
									Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
 | 
				
			||||||
			Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
 | 
									Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
 | 
				
			||||||
			Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
							},
 | 
				
			||||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
 | 
					 | 
				
			||||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,6 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos permissive defaults no enablements", new(stubNixOS),
 | 
							"nixos permissive defaults no enablements", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			Command: make([]string, 0),
 | 
					 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    0,
 | 
									AppID:    0,
 | 
				
			||||||
				Username: "chronos",
 | 
									Username: "chronos",
 | 
				
			||||||
@ -35,136 +34,131 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
								Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
								Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
								Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Clearenv: true,
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Args:  []string{"/run/current-system/sw/bin/zsh"},
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":              "/home/chronos",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":             "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":              "xterm-256color",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"USER":              "chronos",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":   "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS": "user",
 | 
								},
 | 
				
			||||||
				"XDG_SESSION_TYPE":  "tty"},
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user/65534", 8388608, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
							},
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
					 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos permissive defaults chromium", new(stubNixOS),
 | 
							"nixos permissive defaults chromium", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
 | 
								Args: []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    9,
 | 
									AppID:    9,
 | 
				
			||||||
				Groups:   []string{"video"},
 | 
									Groups:   []string{"video"},
 | 
				
			||||||
@ -201,7 +195,7 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
					},
 | 
										},
 | 
				
			||||||
					Filter: true,
 | 
										Filter: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
									Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		fst.ID{
 | 
							fst.ID{
 | 
				
			||||||
@ -254,141 +248,135 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Clearenv: true,
 | 
								Args:  []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":                     "/home/chronos",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"PULSE_SERVER=unix:/run/user/65534/pulse/native",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/65534/pulse/native",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"USER":                     "chronos",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
					 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			DieWithParent: true,
 | 
									Proc("/proc").
 | 
				
			||||||
			AsInit:        true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Tmpfs("/run/user/65534", 8388608, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
									Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
									Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
 | 
							},
 | 
				
			||||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,8 @@ type stubNixOS struct {
 | 
				
			|||||||
	usernameErr map[string]error
 | 
						usernameErr map[string]error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *stubNixOS) Geteuid() int                             { return 1971 }
 | 
					func (s *stubNixOS) Getuid() int                              { return 1971 }
 | 
				
			||||||
 | 
					func (s *stubNixOS) Getgid() int                              { return 100 }
 | 
				
			||||||
func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
					func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
				
			||||||
func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
					func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
				
			||||||
func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
					func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
				
			||||||
@ -54,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch file {
 | 
						switch file {
 | 
				
			||||||
	case "sudo":
 | 
						case "zsh":
 | 
				
			||||||
		return "/run/wrappers/bin/sudo", nil
 | 
							return "/run/current-system/sw/bin/zsh", nil
 | 
				
			||||||
	case "machinectl":
 | 
					 | 
				
			||||||
		return "/home/ophestra/.nix-profile/bin/machinectl", nil
 | 
					 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
							panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,19 +8,19 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type sealTestCase struct {
 | 
					type sealTestCase struct {
 | 
				
			||||||
	name      string
 | 
						name          string
 | 
				
			||||||
	os        sys.State
 | 
						os            sys.State
 | 
				
			||||||
	config    *fst.Config
 | 
						config        *fst.Config
 | 
				
			||||||
	id        fst.ID
 | 
						id            fst.ID
 | 
				
			||||||
	wantSys   *system.I
 | 
						wantSys       *system.I
 | 
				
			||||||
	wantBwrap *bwrap.Config
 | 
						wantContainer *sandbox.Params
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestApp(t *testing.T) {
 | 
					func TestApp(t *testing.T) {
 | 
				
			||||||
@ -30,15 +30,15 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			a := app.NewWithID(tc.id, tc.os)
 | 
								a := app.NewWithID(tc.id, tc.os)
 | 
				
			||||||
			var (
 | 
								var (
 | 
				
			||||||
				gotSys   *system.I
 | 
									gotSys       *system.I
 | 
				
			||||||
				gotBwrap *bwrap.Config
 | 
									gotContainer *sandbox.Params
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			if !t.Run("seal", func(t *testing.T) {
 | 
								if !t.Run("seal", func(t *testing.T) {
 | 
				
			||||||
				if sa, err := a.Seal(tc.config); err != nil {
 | 
									if sa, err := a.Seal(tc.config); err != nil {
 | 
				
			||||||
					t.Errorf("Seal: error = %v", err)
 | 
										t.Errorf("Seal: error = %v", err)
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
 | 
										gotSys, gotContainer = app.AppIParams(a, sa)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}) {
 | 
								}) {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("compare bwrap", func(t *testing.T) {
 | 
								t.Run("compare params", func(t *testing.T) {
 | 
				
			||||||
				if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
 | 
									if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
 | 
				
			||||||
					t.Errorf("seal: bwrap =\n%s\n, want\n%s",
 | 
										t.Errorf("seal: params =\n%s\n, want\n%s",
 | 
				
			||||||
						mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
 | 
											mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										179
									
								
								internal/app/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								internal/app/errors.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,8 +2,8 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
 | 
				
			|||||||
	return a
 | 
						return a
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
 | 
					func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
 | 
				
			||||||
	v := a.(*app)
 | 
						v := a.(*app)
 | 
				
			||||||
	seal := sa.(*outcome)
 | 
						seal := sa.(*outcome)
 | 
				
			||||||
	if v.outcome != seal || v.id != seal.id {
 | 
						if v.outcome != seal || v.id != seal.id {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// used by the parent process
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TryArgv0 calls [Main] if argv0 indicates the process is started from a file named "init".
 | 
					 | 
				
			||||||
func TryArgv0() {
 | 
					 | 
				
			||||||
	if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
 | 
					 | 
				
			||||||
		Main()
 | 
					 | 
				
			||||||
		internal.Exit(0)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,165 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	// time to wait for linger processes after death of initial process
 | 
					 | 
				
			||||||
	residualProcessTimeout = 5 * time.Second
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// everything beyond this point runs within pid namespace
 | 
					 | 
				
			||||||
// proceed with caution!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Main() {
 | 
					 | 
				
			||||||
	// sharing stdout with shim
 | 
					 | 
				
			||||||
	// USE WITH CAUTION
 | 
					 | 
				
			||||||
	fmsg.Prepare("init")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setting this prevents ptrace
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if os.Getpid() != 1 {
 | 
					 | 
				
			||||||
		log.Fatal("this process must run as pid 1")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// receive setup payload
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		payload    Payload
 | 
					 | 
				
			||||||
		closeSetup func() error
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if f, err := proc.Receive(Env, &payload); err != nil {
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrInvalid) {
 | 
					 | 
				
			||||||
			log.Fatal("invalid config descriptor")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrNotSet) {
 | 
					 | 
				
			||||||
			log.Fatal("FORTIFY_INIT not set")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Fatalf("cannot decode init setup payload: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmsg.Store(payload.Verbose)
 | 
					 | 
				
			||||||
		closeSetup = f
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// child does not need to see this
 | 
					 | 
				
			||||||
		if err = os.Unsetenv(Env); err != nil {
 | 
					 | 
				
			||||||
			log.Printf("cannot unset %s: %v", Env, err)
 | 
					 | 
				
			||||||
			// not fatal
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			fmsg.Verbose("received configuration")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// die with parent
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cmd := exec.Command(payload.Argv0)
 | 
					 | 
				
			||||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
					 | 
				
			||||||
	cmd.Args = payload.Argv
 | 
					 | 
				
			||||||
	cmd.Env = os.Environ()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := cmd.Start(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot start %q: %v", payload.Argv0, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fmsg.Suspend()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// close setup pipe as setup is now complete
 | 
					 | 
				
			||||||
	if err := closeSetup(); err != nil {
 | 
					 | 
				
			||||||
		log.Println("cannot close setup pipe:", err)
 | 
					 | 
				
			||||||
		// not fatal
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	sig := make(chan os.Signal, 2)
 | 
					 | 
				
			||||||
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	type winfo struct {
 | 
					 | 
				
			||||||
		wpid    int
 | 
					 | 
				
			||||||
		wstatus syscall.WaitStatus
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	info := make(chan winfo, 1)
 | 
					 | 
				
			||||||
	done := make(chan struct{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	go func() {
 | 
					 | 
				
			||||||
		var (
 | 
					 | 
				
			||||||
			err     error
 | 
					 | 
				
			||||||
			wpid    = -2
 | 
					 | 
				
			||||||
			wstatus syscall.WaitStatus
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// keep going until no child process is left
 | 
					 | 
				
			||||||
		for wpid != -1 {
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if wpid != -2 {
 | 
					 | 
				
			||||||
				info <- winfo{wpid, wstatus}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			err = syscall.EINTR
 | 
					 | 
				
			||||||
			for errors.Is(err, syscall.EINTR) {
 | 
					 | 
				
			||||||
				wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if !errors.Is(err, syscall.ECHILD) {
 | 
					 | 
				
			||||||
			log.Println("unexpected wait4 response:", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		close(done)
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// closed after residualProcessTimeout has elapsed after initial process death
 | 
					 | 
				
			||||||
	timeout := make(chan struct{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	r := 2
 | 
					 | 
				
			||||||
	for {
 | 
					 | 
				
			||||||
		select {
 | 
					 | 
				
			||||||
		case s := <-sig:
 | 
					 | 
				
			||||||
			if fmsg.Resume() {
 | 
					 | 
				
			||||||
				fmsg.Verbosef("terminating on %s after process start", s.String())
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				fmsg.Verbosef("terminating on %s", s.String())
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			internal.Exit(0)
 | 
					 | 
				
			||||||
		case w := <-info:
 | 
					 | 
				
			||||||
			if w.wpid == cmd.Process.Pid {
 | 
					 | 
				
			||||||
				// initial process exited, output is most likely available again
 | 
					 | 
				
			||||||
				fmsg.Resume()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				switch {
 | 
					 | 
				
			||||||
				case w.wstatus.Exited():
 | 
					 | 
				
			||||||
					r = w.wstatus.ExitStatus()
 | 
					 | 
				
			||||||
				case w.wstatus.Signaled():
 | 
					 | 
				
			||||||
					r = 128 + int(w.wstatus.Signal())
 | 
					 | 
				
			||||||
				default:
 | 
					 | 
				
			||||||
					r = 255
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				go func() {
 | 
					 | 
				
			||||||
					time.Sleep(residualProcessTimeout)
 | 
					 | 
				
			||||||
					close(timeout)
 | 
					 | 
				
			||||||
				}()
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		case <-done:
 | 
					 | 
				
			||||||
			internal.Exit(r)
 | 
					 | 
				
			||||||
		case <-timeout:
 | 
					 | 
				
			||||||
			log.Println("timeout exceeded waiting for lingering processes")
 | 
					 | 
				
			||||||
			internal.Exit(r)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_INIT"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// target full exec path
 | 
					 | 
				
			||||||
	Argv0 string
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -3,16 +3,12 @@ package app
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"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/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
@ -20,7 +16,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const shimSetupTimeout = 5 * time.Second
 | 
					const shimSetupTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
					func (seal *outcome) Run(rs *fst.RunState) error {
 | 
				
			||||||
	if !seal.f.CompareAndSwap(false, true) {
 | 
						if !seal.f.CompareAndSwap(false, true) {
 | 
				
			||||||
		// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
							// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
				
			||||||
		// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
							// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
				
			||||||
@ -32,33 +28,15 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		panic("invalid state")
 | 
							panic("invalid state")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						// read comp values early to allow for early failure
 | 
				
			||||||
		resolve exec paths
 | 
						fmsg.Verbosef("version %s", internal.Version())
 | 
				
			||||||
	*/
 | 
						fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	shimExec := [2]string{helper.BubblewrapName}
 | 
					 | 
				
			||||||
	if len(seal.command) > 0 {
 | 
					 | 
				
			||||||
		shimExec[1] = seal.command[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	for i, n := range shimExec {
 | 
					 | 
				
			||||||
		if len(n) == 0 {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if filepath.Base(n) == n {
 | 
					 | 
				
			||||||
			if s, err := exec.LookPath(n); err == nil {
 | 
					 | 
				
			||||||
				shimExec[i] = s
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				return fmsg.WrapError(err,
 | 
					 | 
				
			||||||
					fmt.Sprintf("executable file %q not found in $PATH", n))
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		prepare/revert os state
 | 
							prepare/revert os state
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := seal.sys.Commit(ctx); err != nil {
 | 
						if err := seal.sys.Commit(seal.ctx); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	store := state.NewMulti(seal.runDirPath)
 | 
						store := state.NewMulti(seal.runDirPath)
 | 
				
			||||||
@ -74,16 +52,16 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
					revert app setup transaction
 | 
										revert app setup transaction
 | 
				
			||||||
				*/
 | 
									*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				rt, ec := new(system.Enablements), new(system.Criteria)
 | 
									var rt system.Enablement
 | 
				
			||||||
				ec.Enablements = new(system.Enablements)
 | 
									ec := system.Process
 | 
				
			||||||
				ec.Set(system.Process)
 | 
					 | 
				
			||||||
				if states, err := c.Load(); err != nil {
 | 
									if states, err := c.Load(); err != nil {
 | 
				
			||||||
					// revert per-process state here to limit damage
 | 
										// revert per-process state here to limit damage
 | 
				
			||||||
					return errors.Join(err, seal.sys.Revert(ec))
 | 
										storeErr.OpErr = err
 | 
				
			||||||
 | 
										return seal.sys.Revert((*system.Criteria)(&ec))
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					if l := len(states); l == 0 {
 | 
										if l := len(states); l == 0 {
 | 
				
			||||||
						fmsg.Verbose("no other launchers active, will clean up globals")
 | 
											fmsg.Verbose("no other launchers active, will clean up globals")
 | 
				
			||||||
						ec.Set(system.User)
 | 
											ec |= system.User
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
 | 
											fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@ -91,38 +69,23 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
					// accumulate enablements of remaining launchers
 | 
										// accumulate enablements of remaining launchers
 | 
				
			||||||
					for i, s := range states {
 | 
										for i, s := range states {
 | 
				
			||||||
						if s.Config != nil {
 | 
											if s.Config != nil {
 | 
				
			||||||
							*rt |= s.Config.Confinement.Enablements
 | 
												rt |= s.Config.Confinement.Enablements
 | 
				
			||||||
						} else {
 | 
											} else {
 | 
				
			||||||
							log.Printf("state entry %d does not contain config", i)
 | 
												log.Printf("state entry %d does not contain config", i)
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// invert accumulated enablements for cleanup
 | 
									ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
 | 
				
			||||||
				for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
 | 
					 | 
				
			||||||
					if !rt.Has(i) {
 | 
					 | 
				
			||||||
						ec.Set(i)
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if fmsg.Load() {
 | 
									if fmsg.Load() {
 | 
				
			||||||
					labels := make([]string, 0, system.ELen+1)
 | 
										if ec > 0 {
 | 
				
			||||||
					for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
 | 
											fmsg.Verbose("reverting operations type", system.TypeString(ec))
 | 
				
			||||||
						if ec.Has(i) {
 | 
					 | 
				
			||||||
							labels = append(labels, system.TypeString(i))
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if len(labels) > 0 {
 | 
					 | 
				
			||||||
						fmsg.Verbose("reverting operations type", strings.Join(labels, ", "))
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				err := seal.sys.Revert(ec)
 | 
									return seal.sys.Revert((*system.Criteria)(&ec))
 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					err = err.(RevertCompoundError)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			}()
 | 
								}()
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		storeErr.Err = errors.Join(revertErr, store.Close())
 | 
							storeErr.save([]error{revertErr, store.Close()})
 | 
				
			||||||
		rs.RevertErr = storeErr.equiv("error returned during cleanup:")
 | 
							rs.RevertErr = storeErr.equiv("error returned during cleanup:")
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,11 +94,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	waitErr := make(chan error, 1)
 | 
						waitErr := make(chan error, 1)
 | 
				
			||||||
	cmd := new(shim.Shim)
 | 
						cmd := new(shimProcess)
 | 
				
			||||||
	if startTime, err := cmd.Start(
 | 
						if startTime, err := cmd.Start(
 | 
				
			||||||
		seal.user.aid.String(),
 | 
							seal.user.aid.String(),
 | 
				
			||||||
		seal.user.supp,
 | 
							seal.user.supp,
 | 
				
			||||||
		seal.bwrapSync,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -143,7 +105,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		rs.Time = startTime
 | 
							rs.Time = startTime
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
 | 
						ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
@ -152,11 +114,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		cancel()
 | 
							cancel()
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := cmd.Serve(c, &shim.Payload{
 | 
						if err := cmd.Serve(ctx, &shimParams{
 | 
				
			||||||
		Argv:  seal.command,
 | 
							Container: seal.container,
 | 
				
			||||||
		Exec:  shimExec,
 | 
							Home:      seal.user.data,
 | 
				
			||||||
		Bwrap: seal.container,
 | 
					 | 
				
			||||||
		Home:  seal.user.data,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Verbose: fmsg.Load(),
 | 
							Verbose: fmsg.Load(),
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
@ -170,7 +130,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		Time: *rs.Time,
 | 
							Time: *rs.Time,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
 | 
						var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
 | 
				
			||||||
	earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.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
 | 
						// destroy defunct state entry
 | 
				
			||||||
	deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
 | 
						deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -195,86 +157,24 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	// this is reached when a fault makes an already running shim impossible to continue execution
 | 
						// this is reached when a fault makes an already running shim impossible to continue execution
 | 
				
			||||||
	// however a kill signal could not be delivered (should actually always happen like that since fsu)
 | 
						// however a kill signal could not be delivered (should actually always happen like that since fsu)
 | 
				
			||||||
	// the effects of this is similar to the alternative exit path and ensures shim death
 | 
						// the effects of this is similar to the alternative exit path and ensures shim death
 | 
				
			||||||
	case err := <-cmd.WaitFallback():
 | 
						case err := <-cmd.Fallback():
 | 
				
			||||||
		rs.ExitCode = 255
 | 
							rs.ExitCode = 255
 | 
				
			||||||
		log.Printf("cannot terminate shim on faulted setup: %v", err)
 | 
							log.Printf("cannot terminate shim on faulted setup: %v", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// alternative exit path relying on shim behaviour on monitor process exit
 | 
						// alternative exit path relying on shim behaviour on monitor process exit
 | 
				
			||||||
	case <-ctx.Done():
 | 
						case <-seal.ctx.Done():
 | 
				
			||||||
		fmsg.Verbose("alternative exit path selected")
 | 
							fmsg.Verbose("alternative exit path selected")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Resume()
 | 
						fmsg.Resume()
 | 
				
			||||||
 | 
						if seal.sync != nil {
 | 
				
			||||||
 | 
							if err := seal.sync.Close(); err != nil {
 | 
				
			||||||
 | 
								log.Printf("cannot close wayland security context: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if seal.dbusMsg != nil {
 | 
						if seal.dbusMsg != nil {
 | 
				
			||||||
		// dump dbus message buffer
 | 
					 | 
				
			||||||
		seal.dbusMsg()
 | 
							seal.dbusMsg()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return earlyStoreErr.equiv("cannot save process state:")
 | 
						return earlyStoreErr.equiv("cannot save process state:")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// StateStoreError is returned for a failed state save
 | 
					 | 
				
			||||||
type StateStoreError struct {
 | 
					 | 
				
			||||||
	// whether inner function was called
 | 
					 | 
				
			||||||
	Inner bool
 | 
					 | 
				
			||||||
	// returned by the Do method of [state.Store]
 | 
					 | 
				
			||||||
	DoErr error
 | 
					 | 
				
			||||||
	// returned by the Save/Destroy method of [state.Cursor]
 | 
					 | 
				
			||||||
	InnerErr error
 | 
					 | 
				
			||||||
	// stores an arbitrary error
 | 
					 | 
				
			||||||
	Err error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// save saves exactly one arbitrary error in [StateStoreError].
 | 
					 | 
				
			||||||
func (e *StateStoreError) save(err error) {
 | 
					 | 
				
			||||||
	if err == nil || e.Err != nil {
 | 
					 | 
				
			||||||
		panic("invalid call to save")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	e.Err = err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) equiv(a ...any) error {
 | 
					 | 
				
			||||||
	if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return fmsg.WrapErrorSuffix(e, a...)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) Error() string {
 | 
					 | 
				
			||||||
	if e.Inner && e.InnerErr != nil {
 | 
					 | 
				
			||||||
		return e.InnerErr.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if e.DoErr != nil {
 | 
					 | 
				
			||||||
		return e.DoErr.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if e.Err != nil {
 | 
					 | 
				
			||||||
		return e.Err.Error()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// equiv nullifies e for values where this is reached
 | 
					 | 
				
			||||||
	panic("unreachable")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *StateStoreError) Unwrap() (errs []error) {
 | 
					 | 
				
			||||||
	errs = make([]error, 0, 3)
 | 
					 | 
				
			||||||
	if e.DoErr != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.DoErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.InnerErr != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.InnerErr)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if e.Err != nil {
 | 
					 | 
				
			||||||
		errs = append(errs, e.Err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// A RevertCompoundError encapsulates errors returned by
 | 
					 | 
				
			||||||
// the Revert method of [system.I].
 | 
					 | 
				
			||||||
type RevertCompoundError interface {
 | 
					 | 
				
			||||||
	Error() string
 | 
					 | 
				
			||||||
	Unwrap() []error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -2,26 +2,30 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/wl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/wl"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@ -65,19 +69,19 @@ type outcome struct {
 | 
				
			|||||||
	// copied from [sys.State] response
 | 
						// copied from [sys.State] response
 | 
				
			||||||
	runDirPath string
 | 
						runDirPath string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through from [fst.Config]
 | 
					 | 
				
			||||||
	command []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// initial [fst.Config] gob stream for state data;
 | 
						// initial [fst.Config] gob stream for state data;
 | 
				
			||||||
	// this is prepared ahead of time as config is mutated during seal creation
 | 
						// this is prepared ahead of time as config is clobbered during seal creation
 | 
				
			||||||
	ct io.WriterTo
 | 
						ct io.WriterTo
 | 
				
			||||||
	// dump dbus proxy message buffer
 | 
						// dump dbus proxy message buffer
 | 
				
			||||||
	dbusMsg func()
 | 
						dbusMsg func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user      fsuUser
 | 
						user fsuUser
 | 
				
			||||||
	sys       *system.I
 | 
						sys  *system.I
 | 
				
			||||||
	container *bwrap.Config
 | 
						ctx  context.Context
 | 
				
			||||||
	bwrapSync *os.File
 | 
					
 | 
				
			||||||
 | 
						container *sandbox.Params
 | 
				
			||||||
 | 
						env       map[string]string
 | 
				
			||||||
 | 
						sync      *os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	f atomic.Bool
 | 
						f atomic.Bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -100,7 +104,17 @@ type fsuUser struct {
 | 
				
			|||||||
	username string
 | 
						username string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
					func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
 | 
				
			||||||
 | 
						if seal.ctx != nil {
 | 
				
			||||||
 | 
							panic("finalise called twice")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						seal.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shellPath := "/bin/sh"
 | 
				
			||||||
 | 
						if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
 | 
				
			||||||
 | 
							shellPath = s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// encode initial configuration for state tracking
 | 
							// encode initial configuration for state tracking
 | 
				
			||||||
		ct := new(bytes.Buffer)
 | 
							ct := new(bytes.Buffer)
 | 
				
			||||||
@ -111,9 +125,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.ct = ct
 | 
							seal.ct = ct
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass through command slice; this value is never touched in the main process
 | 
					 | 
				
			||||||
	seal.command = config.Command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// allowed aid range 0 to 9999, this is checked again in fsu
 | 
						// allowed aid range 0 to 9999, this is checked again in fsu
 | 
				
			||||||
	if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
						if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
				
			||||||
		return fmsg.WrapError(ErrUser,
 | 
							return fmsg.WrapError(ErrUser,
 | 
				
			||||||
@ -167,12 +178,24 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if config.Confinement.Sandbox == nil {
 | 
						if config.Confinement.Sandbox == nil {
 | 
				
			||||||
		fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
							fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fsu clears the environment so resolve paths early
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								if len(config.Args) > 0 {
 | 
				
			||||||
 | 
									if p, err := sys.LookPath(config.Args[0]); err != nil {
 | 
				
			||||||
 | 
										return fmsg.WrapError(err, err.Error())
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										config.Path = p
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									config.Path = shellPath
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf := &fst.SandboxConfig{
 | 
							conf := &fst.SandboxConfig{
 | 
				
			||||||
			UserNS:       true,
 | 
								Userns:  true,
 | 
				
			||||||
			Net:          true,
 | 
								Net:     true,
 | 
				
			||||||
			Syscall:      new(bwrap.SyscallPolicy),
 | 
								Tty:     true,
 | 
				
			||||||
			NoNewSession: true,
 | 
								AutoEtc: true,
 | 
				
			||||||
			AutoEtc:      true,
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind entries in /
 | 
							// bind entries in /
 | 
				
			||||||
		if d, err := sys.ReadDir("/"); err != nil {
 | 
							if d, err := sys.ReadDir("/"); err != nil {
 | 
				
			||||||
@ -198,10 +221,10 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		// hide nscd from sandbox if present
 | 
							// hide nscd from sandbox if present
 | 
				
			||||||
		nscd := "/var/run/nscd"
 | 
							nscd := "/var/run/nscd"
 | 
				
			||||||
		if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
							if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
			conf.Override = append(conf.Override, nscd)
 | 
								conf.Cover = append(conf.Cover, nscd)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind GPU stuff
 | 
							// bind GPU stuff
 | 
				
			||||||
		if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
 | 
							if config.Confinement.Enablements&(system.EX11|system.EWayland) != 0 {
 | 
				
			||||||
			conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
 | 
								conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// opportunistically bind kvm
 | 
							// opportunistically bind kvm
 | 
				
			||||||
@ -210,17 +233,29 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		config.Confinement.Sandbox = conf
 | 
							config.Confinement.Sandbox = conf
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var mapuid *stringPair[int]
 | 
						var mapuid, mapgid *stringPair[int]
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		var uid int
 | 
							var uid, gid int
 | 
				
			||||||
		var err error
 | 
							var err error
 | 
				
			||||||
		seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
 | 
							seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
									"cannot initialise container configuration:")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.EINVAL,
 | 
				
			||||||
 | 
									"invalid program path")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(config.Args) == 0 {
 | 
				
			||||||
 | 
								config.Args = []string{config.Path}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							seal.container.Path = config.Path
 | 
				
			||||||
 | 
							seal.container.Args = config.Args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mapuid = newInt(uid)
 | 
							mapuid = newInt(uid)
 | 
				
			||||||
		if seal.container.SetEnv == nil {
 | 
							mapgid = newInt(gid)
 | 
				
			||||||
			seal.container.SetEnv = make(map[string]string)
 | 
							if seal.env == nil {
 | 
				
			||||||
 | 
								seal.env = make(map[string]string)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -231,10 +266,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	sc := sys.Paths()
 | 
						sc := sys.Paths()
 | 
				
			||||||
	seal.runDirPath = sc.RunDirPath
 | 
						seal.runDirPath = sc.RunDirPath
 | 
				
			||||||
	seal.sys = system.New(seal.user.uid.unwrap())
 | 
						seal.sys = system.New(seal.user.uid.unwrap())
 | 
				
			||||||
	seal.sys.IsVerbose = fmsg.Load
 | 
					 | 
				
			||||||
	seal.sys.Verbose = fmsg.Verbose
 | 
					 | 
				
			||||||
	seal.sys.Verbosef = fmsg.Verbosef
 | 
					 | 
				
			||||||
	seal.sys.WrapErr = fmsg.WrapError
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Work directories
 | 
							Work directories
 | 
				
			||||||
@ -259,35 +290,27 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
						// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
				
			||||||
	innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
						innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
				
			||||||
	seal.container.Tmpfs("/run/user", 1*1024*1024)
 | 
						seal.container.Tmpfs("/run/user", 1<<12, 0755)
 | 
				
			||||||
	seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
 | 
						seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755)
 | 
				
			||||||
	seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
 | 
						seal.env[xdgRuntimeDir] = innerRuntimeDir
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionClass] = "user"
 | 
						seal.env[xdgSessionClass] = "user"
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionType] = "tty"
 | 
						seal.env[xdgSessionType] = "tty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// outer path for inner /tmp
 | 
						// outer path for inner /tmp
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
							tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
				
			||||||
		seal.sys.Ensure(tmpdir, 0700)
 | 
							seal.sys.Ensure(tmpdir, 0700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
				
			||||||
		tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
 | 
							tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
 | 
				
			||||||
		seal.sys.Ensure(tmpdirProc, 01700)
 | 
							seal.sys.Ensure(tmpdirInst, 01700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		seal.container.Bind(tmpdirProc, "/tmp", false, true)
 | 
							seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Passwd database
 | 
							Passwd database
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// look up shell
 | 
					 | 
				
			||||||
	sh := "/bin/sh"
 | 
					 | 
				
			||||||
	if s, ok := sys.LookupEnv(shell); ok {
 | 
					 | 
				
			||||||
		seal.container.SetEnv[shell] = s
 | 
					 | 
				
			||||||
		sh = s
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// bind home directory
 | 
					 | 
				
			||||||
	homeDir := "/var/empty"
 | 
						homeDir := "/var/empty"
 | 
				
			||||||
	if seal.user.home != "" {
 | 
						if seal.user.home != "" {
 | 
				
			||||||
		homeDir = seal.user.home
 | 
							homeDir = seal.user.home
 | 
				
			||||||
@ -296,28 +319,26 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if seal.user.username != "" {
 | 
						if seal.user.username != "" {
 | 
				
			||||||
		username = seal.user.username
 | 
							username = seal.user.username
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	seal.container.Bind(seal.user.data, homeDir, false, true)
 | 
						seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
 | 
				
			||||||
	seal.container.Chdir = homeDir
 | 
						seal.container.Dir = homeDir
 | 
				
			||||||
	seal.container.SetEnv["HOME"] = homeDir
 | 
						seal.env["HOME"] = homeDir
 | 
				
			||||||
	seal.container.SetEnv["USER"] = username
 | 
						seal.env["USER"] = username
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// generate /etc/passwd and /etc/group
 | 
						seal.container.Place("/etc/passwd",
 | 
				
			||||||
	seal.container.CopyBind("/etc/passwd",
 | 
							[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
 | 
				
			||||||
		[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
 | 
						seal.container.Place("/etc/group",
 | 
				
			||||||
	seal.container.CopyBind("/etc/group",
 | 
							[]byte("fortify:x:"+mapgid.String()+":\n"))
 | 
				
			||||||
		[]byte("fortify:x:"+mapuid.String()+":\n"))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Display servers
 | 
							Display servers
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass $TERM to launcher
 | 
						// pass $TERM for proper terminal I/O in shell
 | 
				
			||||||
	if t, ok := sys.LookupEnv(term); ok {
 | 
						if t, ok := sys.LookupEnv(term); ok {
 | 
				
			||||||
		seal.container.SetEnv[term] = t
 | 
							seal.env[term] = t
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up wayland
 | 
						if config.Confinement.Enablements&system.EWayland != 0 {
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EWayland) {
 | 
					 | 
				
			||||||
		// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
							// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
				
			||||||
		var socketPath string
 | 
							var socketPath string
 | 
				
			||||||
		if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
 | 
							if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
 | 
				
			||||||
@ -330,7 +351,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
							innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
				
			||||||
		seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
 | 
							seal.env[wl.WaylandDisplay] = wl.FallbackName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
							if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
				
			||||||
			socketDir := path.Join(sc.SharePath, "wayland")
 | 
								socketDir := path.Join(sc.SharePath, "wayland")
 | 
				
			||||||
@ -341,25 +362,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
				// use instance ID in case app id is not set
 | 
									// use instance ID in case app id is not set
 | 
				
			||||||
				appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
									appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
 | 
								seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
 | 
				
			||||||
			seal.container.Bind(outerPath, innerPath)
 | 
								seal.container.Bind(outerPath, innerPath, 0)
 | 
				
			||||||
		} else { // bind mount wayland socket (insecure)
 | 
							} else { // bind mount wayland socket (insecure)
 | 
				
			||||||
			fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
								fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
				
			||||||
			seal.container.Bind(socketPath, innerPath)
 | 
								seal.container.Bind(socketPath, innerPath, 0)
 | 
				
			||||||
			seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
								seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up X11
 | 
						if config.Confinement.Enablements&system.EX11 != 0 {
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EX11) {
 | 
					 | 
				
			||||||
		// discover X11 and grant user permission via the `ChangeHosts` command
 | 
					 | 
				
			||||||
		if d, ok := sys.LookupEnv(display); !ok {
 | 
							if d, ok := sys.LookupEnv(display); !ok {
 | 
				
			||||||
			return fmsg.WrapError(ErrXDisplay,
 | 
								return fmsg.WrapError(ErrXDisplay,
 | 
				
			||||||
				"DISPLAY is not set")
 | 
									"DISPLAY is not set")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
								seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
				
			||||||
			seal.container.SetEnv[display] = d
 | 
								seal.env[display] = d
 | 
				
			||||||
			seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
 | 
								seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -367,7 +386,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		PulseAudio server and authentication
 | 
							PulseAudio server and authentication
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EPulse) {
 | 
						if config.Confinement.Enablements&system.EPulse != 0 {
 | 
				
			||||||
		// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
 | 
							// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
 | 
				
			||||||
		pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
 | 
							pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
 | 
				
			||||||
		// PulseAudio socket (usually `/run/user/%d/pulse/native`)
 | 
							// PulseAudio socket (usually `/run/user/%d/pulse/native`)
 | 
				
			||||||
@ -400,8 +419,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
							innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
				
			||||||
		innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
							innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
				
			||||||
		seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
							seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
				
			||||||
		seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
 | 
							seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
 | 
				
			||||||
		seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
 | 
							seal.env[pulseServer] = "unix:" + innerPulseSocket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// publish current user's pulse cookie for target user
 | 
							// publish current user's pulse cookie for target user
 | 
				
			||||||
		if src, err := discoverPulseCookie(sys); err != nil {
 | 
							if src, err := discoverPulseCookie(sys); err != nil {
 | 
				
			||||||
@ -409,9 +428,9 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
			fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
								fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
								innerDst := fst.Tmp + "/pulse-cookie"
 | 
				
			||||||
			seal.container.SetEnv[pulseCookie] = innerDst
 | 
								seal.env[pulseCookie] = innerDst
 | 
				
			||||||
			payload := new([]byte)
 | 
								var payload *[]byte
 | 
				
			||||||
			seal.container.CopyBindRef(innerDst, &payload)
 | 
								seal.container.PlaceP(innerDst, &payload)
 | 
				
			||||||
			seal.sys.CopyFile(payload, src, 256, 256)
 | 
								seal.sys.CopyFile(payload, src, 256, 256)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -420,7 +439,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		D-Bus proxy
 | 
							D-Bus proxy
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EDBus) {
 | 
						if config.Confinement.Enablements&system.EDBus != 0 {
 | 
				
			||||||
		// ensure dbus session bus defaults
 | 
							// ensure dbus session bus defaults
 | 
				
			||||||
		if config.Confinement.SessionBus == nil {
 | 
							if config.Confinement.SessionBus == nil {
 | 
				
			||||||
			config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
 | 
								config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
 | 
				
			||||||
@ -441,13 +460,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// share proxy sockets
 | 
							// share proxy sockets
 | 
				
			||||||
		sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
							sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
				
			||||||
		seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
							seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
				
			||||||
		seal.container.Bind(sessionPath, sessionInner)
 | 
							seal.container.Bind(sessionPath, sessionInner, 0)
 | 
				
			||||||
		seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
							seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
				
			||||||
		if config.Confinement.SystemBus != nil {
 | 
							if config.Confinement.SystemBus != nil {
 | 
				
			||||||
			systemInner := "/run/dbus/system_bus_socket"
 | 
								systemInner := "/run/dbus/system_bus_socket"
 | 
				
			||||||
			seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
								seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
				
			||||||
			seal.container.Bind(systemPath, systemInner)
 | 
								seal.container.Bind(systemPath, systemInner, 0)
 | 
				
			||||||
			seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
								seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -456,9 +475,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		Miscellaneous
 | 
							Miscellaneous
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// queue overriding tmpfs at the end of seal.container.Filesystem
 | 
						for _, dest := range config.Confinement.Sandbox.Cover {
 | 
				
			||||||
	for _, dest := range config.Confinement.Sandbox.Override {
 | 
							seal.container.Tmpfs(dest, 1<<13, 0755)
 | 
				
			||||||
		seal.container.Tmpfs(dest, 8*1024)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// append ExtraPerms last
 | 
						// append ExtraPerms last
 | 
				
			||||||
@ -484,12 +502,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
							seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// mount fortify in sandbox for init
 | 
						// flatten and sort env for deterministic behaviour
 | 
				
			||||||
	seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
 | 
						seal.container.Env = make([]string, 0, len(seal.env))
 | 
				
			||||||
	seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
 | 
						maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
 | 
				
			||||||
 | 
						slices.Sort(seal.container.Env)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
 | 
						fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
 | 
				
			||||||
		seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
 | 
							seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										212
									
								
								internal/app/shim.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								internal/app/shim.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,212 @@
 | 
				
			|||||||
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const shimEnv = "FORTIFY_SHIM"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type shimParams struct {
 | 
				
			||||||
 | 
						// finalised container params
 | 
				
			||||||
 | 
						Container *sandbox.Params
 | 
				
			||||||
 | 
						// path to outer home directory
 | 
				
			||||||
 | 
						Home string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verbosity pass through
 | 
				
			||||||
 | 
						Verbose bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ShimMain is the main function of the shim process and runs as the unconstrained target user.
 | 
				
			||||||
 | 
					func ShimMain() {
 | 
				
			||||||
 | 
						fmsg.Prepare("shim")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							params     shimParams
 | 
				
			||||||
 | 
							closeSetup func() error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if f, err := sandbox.Receive(shimEnv, ¶ms, nil); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, sandbox.ErrInvalid) {
 | 
				
			||||||
 | 
								log.Fatal("invalid config descriptor")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, sandbox.ErrNotSet) {
 | 
				
			||||||
 | 
								log.Fatal("FORTIFY_SHIM not set")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Fatalf("cannot receive shim setup params: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							internal.InstallFmsg(params.Verbose)
 | 
				
			||||||
 | 
							closeSetup = f
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if params.Container == nil || params.Container.Ops == nil {
 | 
				
			||||||
 | 
							log.Fatal("invalid container params")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// close setup socket
 | 
				
			||||||
 | 
						if err := closeSetup(); err != nil {
 | 
				
			||||||
 | 
							log.Printf("cannot close setup pipe: %v", err)
 | 
				
			||||||
 | 
							// not fatal
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ensure home directory as target user
 | 
				
			||||||
 | 
						if s, err := os.Stat(params.Home); err != nil {
 | 
				
			||||||
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
 | 
								if err = os.Mkdir(params.Home, 0700); err != nil {
 | 
				
			||||||
 | 
									log.Fatalf("cannot create home directory: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								log.Fatalf("cannot access home directory: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// home directory is created, proceed
 | 
				
			||||||
 | 
						} else if !s.IsDir() {
 | 
				
			||||||
 | 
							log.Fatalf("path %q is not a directory", params.Home)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var name string
 | 
				
			||||||
 | 
						if len(params.Container.Args) > 0 {
 | 
				
			||||||
 | 
							name = params.Container.Args[0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
				
			||||||
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
						container := sandbox.New(ctx, name)
 | 
				
			||||||
 | 
						container.Params = *params.Container
 | 
				
			||||||
 | 
						container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
 | 
				
			||||||
 | 
						container.WaitDelay = 2 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := container.Start(); err != nil {
 | 
				
			||||||
 | 
							fmsg.PrintBaseError(err, "cannot start container:")
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Serve(); err != nil {
 | 
				
			||||||
 | 
							fmsg.PrintBaseError(err, "cannot configure container:")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Wait(); err != nil {
 | 
				
			||||||
 | 
							var exitError *exec.ExitError
 | 
				
			||||||
 | 
							if !errors.As(err, &exitError) {
 | 
				
			||||||
 | 
								if errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
									os.Exit(2)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								log.Printf("wait: %v", err)
 | 
				
			||||||
 | 
								os.Exit(127)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							os.Exit(exitError.ExitCode())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type shimProcess struct {
 | 
				
			||||||
 | 
						// user switcher process
 | 
				
			||||||
 | 
						cmd *exec.Cmd
 | 
				
			||||||
 | 
						// fallback exit notifier with error returned killing the process
 | 
				
			||||||
 | 
						killFallback chan error
 | 
				
			||||||
 | 
						// monitor to shim encoder
 | 
				
			||||||
 | 
						encoder *gob.Encoder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Unwrap() *exec.Cmd    { return s.cmd }
 | 
				
			||||||
 | 
					func (s *shimProcess) Fallback() chan error { return s.killFallback }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) String() string {
 | 
				
			||||||
 | 
						if s.cmd == nil {
 | 
				
			||||||
 | 
							return "(unused shim manager)"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.cmd.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Start(
 | 
				
			||||||
 | 
						aid string,
 | 
				
			||||||
 | 
						supp []string,
 | 
				
			||||||
 | 
					) (*time.Time, error) {
 | 
				
			||||||
 | 
						// prepare user switcher invocation
 | 
				
			||||||
 | 
						fsuPath := internal.MustFsuPath()
 | 
				
			||||||
 | 
						s.cmd = exec.Command(fsuPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// pass shim setup pipe
 | 
				
			||||||
 | 
						if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
				
			||||||
 | 
							return nil, fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
								"cannot create shim setup pipe:")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							s.encoder = e
 | 
				
			||||||
 | 
							s.cmd.Env = []string{
 | 
				
			||||||
 | 
								shimEnv + "=" + strconv.Itoa(fd),
 | 
				
			||||||
 | 
								"FORTIFY_APP_ID=" + aid,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// format fsu supplementary groups
 | 
				
			||||||
 | 
						if len(supp) > 0 {
 | 
				
			||||||
 | 
							fmsg.Verbosef("attaching supplementary group ids %s", supp)
 | 
				
			||||||
 | 
							s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						s.cmd.Dir = "/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
				
			||||||
 | 
						// withhold messages to stderr
 | 
				
			||||||
 | 
						fmsg.Suspend()
 | 
				
			||||||
 | 
						if err := s.cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							return nil, fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
								"cannot start fsu:")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						startTime := time.Now().UTC()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &startTime, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Serve(ctx context.Context, params *shimParams) error {
 | 
				
			||||||
 | 
						// kill shim if something goes wrong and an error is returned
 | 
				
			||||||
 | 
						s.killFallback = make(chan error, 1)
 | 
				
			||||||
 | 
						killShim := func() {
 | 
				
			||||||
 | 
							if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
 | 
				
			||||||
 | 
								s.killFallback <- err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() { killShim() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encodeErr := make(chan error)
 | 
				
			||||||
 | 
						go func() { encodeErr <- s.encoder.Encode(params) }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						// encode return indicates setup completion
 | 
				
			||||||
 | 
						case err := <-encodeErr:
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
									"cannot transmit shim config:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							killShim = func() {}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// setup canceled before payload was accepted
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							err := ctx.Err()
 | 
				
			||||||
 | 
							if errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.ECANCELED,
 | 
				
			||||||
 | 
									"shim setup canceled")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, context.DeadlineExceeded) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.ETIMEDOUT,
 | 
				
			||||||
 | 
									"deadline exceeded waiting for shim")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// unreachable
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,153 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"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/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// everything beyond this point runs as unconstrained target user
 | 
					 | 
				
			||||||
// proceed with caution!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Main() {
 | 
					 | 
				
			||||||
	// sharing stdout with fortify
 | 
					 | 
				
			||||||
	// USE WITH CAUTION
 | 
					 | 
				
			||||||
	fmsg.Prepare("shim")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setting this prevents ptrace
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// receive setup payload
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		payload    Payload
 | 
					 | 
				
			||||||
		closeSetup func() error
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if f, err := proc.Receive(Env, &payload); err != nil {
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrInvalid) {
 | 
					 | 
				
			||||||
			log.Fatal("invalid config descriptor")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrNotSet) {
 | 
					 | 
				
			||||||
			log.Fatal("FORTIFY_SHIM not set")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Fatalf("cannot decode shim setup payload: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmsg.Store(payload.Verbose)
 | 
					 | 
				
			||||||
		closeSetup = f
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if payload.Bwrap == nil {
 | 
					 | 
				
			||||||
		log.Fatal("bwrap config not supplied")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// restore bwrap sync fd
 | 
					 | 
				
			||||||
	var syncFd *os.File
 | 
					 | 
				
			||||||
	if payload.Sync != nil {
 | 
					 | 
				
			||||||
		syncFd = os.NewFile(*payload.Sync, "sync")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// close setup socket
 | 
					 | 
				
			||||||
	if err := closeSetup(); err != nil {
 | 
					 | 
				
			||||||
		log.Println("cannot close setup pipe:", err)
 | 
					 | 
				
			||||||
		// not fatal
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ensure home directory as target user
 | 
					 | 
				
			||||||
	if s, err := os.Stat(payload.Home); err != nil {
 | 
					 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
					 | 
				
			||||||
			if err = os.Mkdir(payload.Home, 0700); err != nil {
 | 
					 | 
				
			||||||
				log.Fatalf("cannot create home directory: %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			log.Fatalf("cannot access home directory: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// home directory is created, proceed
 | 
					 | 
				
			||||||
	} else if !s.IsDir() {
 | 
					 | 
				
			||||||
		log.Fatalf("data path %q is not a directory", payload.Home)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var ic init0.Payload
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// resolve argv0
 | 
					 | 
				
			||||||
	ic.Argv = payload.Argv
 | 
					 | 
				
			||||||
	if len(ic.Argv) > 0 {
 | 
					 | 
				
			||||||
		// looked up from $PATH by parent
 | 
					 | 
				
			||||||
		ic.Argv0 = payload.Exec[1]
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// no argv, look up shell instead
 | 
					 | 
				
			||||||
		var ok bool
 | 
					 | 
				
			||||||
		if payload.Bwrap.SetEnv == nil {
 | 
					 | 
				
			||||||
			log.Fatal("no command was specified and environment is unset")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
 | 
					 | 
				
			||||||
			log.Fatal("no command was specified and $SHELL was unset")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ic.Argv = []string{ic.Argv0}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conf := payload.Bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var extraFiles []*os.File
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// serve setup payload
 | 
					 | 
				
			||||||
	if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot pipe: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		conf.SetEnv[init0.Env] = strconv.Itoa(fd)
 | 
					 | 
				
			||||||
		go func() {
 | 
					 | 
				
			||||||
			fmsg.Verbose("transmitting config to init")
 | 
					 | 
				
			||||||
			if err = encoder.Encode(&ic); err != nil {
 | 
					 | 
				
			||||||
				log.Fatalf("cannot transmit init config: %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
					 | 
				
			||||||
	if fmsg.Load() {
 | 
					 | 
				
			||||||
		seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if b, err := helper.NewBwrap(
 | 
					 | 
				
			||||||
		conf, path.Join(fst.Tmp, "sbin/init"),
 | 
					 | 
				
			||||||
		nil, func(int, int) []string { return make([]string, 0) },
 | 
					 | 
				
			||||||
		extraFiles,
 | 
					 | 
				
			||||||
		syncFd,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("malformed sandbox config: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(os.Stderr)
 | 
					 | 
				
			||||||
		ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
					 | 
				
			||||||
		defer stop() // unreachable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// run and pass through exit code
 | 
					 | 
				
			||||||
		if err = b.Start(ctx, false); err != nil {
 | 
					 | 
				
			||||||
			log.Fatalf("cannot start target process: %v", err)
 | 
					 | 
				
			||||||
		} else if err = b.Wait(); err != nil {
 | 
					 | 
				
			||||||
			var exitError *exec.ExitError
 | 
					 | 
				
			||||||
			if !errors.As(err, &exitError) {
 | 
					 | 
				
			||||||
				log.Printf("wait: %v", err)
 | 
					 | 
				
			||||||
				internal.Exit(127)
 | 
					 | 
				
			||||||
				panic("unreachable")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			internal.Exit(exitError.ExitCode())
 | 
					 | 
				
			||||||
			panic("unreachable")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,139 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// used by the parent process
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Shim struct {
 | 
					 | 
				
			||||||
	// user switcher process
 | 
					 | 
				
			||||||
	cmd *exec.Cmd
 | 
					 | 
				
			||||||
	// fallback exit notifier with error returned killing the process
 | 
					 | 
				
			||||||
	killFallback chan error
 | 
					 | 
				
			||||||
	// monitor to shim encoder
 | 
					 | 
				
			||||||
	encoder *gob.Encoder
 | 
					 | 
				
			||||||
	// bwrap --sync-fd value
 | 
					 | 
				
			||||||
	sync *uintptr
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) String() string {
 | 
					 | 
				
			||||||
	if s.cmd == nil {
 | 
					 | 
				
			||||||
		return "(unused shim manager)"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return s.cmd.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Unwrap() *exec.Cmd {
 | 
					 | 
				
			||||||
	return s.cmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) WaitFallback() chan error {
 | 
					 | 
				
			||||||
	return s.killFallback
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Start(
 | 
					 | 
				
			||||||
	// string representation of application id
 | 
					 | 
				
			||||||
	aid string,
 | 
					 | 
				
			||||||
	// string representation of supplementary group ids
 | 
					 | 
				
			||||||
	supp []string,
 | 
					 | 
				
			||||||
	// bwrap --sync-fd
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) (*time.Time, error) {
 | 
					 | 
				
			||||||
	// prepare user switcher invocation
 | 
					 | 
				
			||||||
	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 {
 | 
					 | 
				
			||||||
		return nil, fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
			"cannot create shim setup pipe:")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		s.encoder = e
 | 
					 | 
				
			||||||
		s.cmd.Env = []string{
 | 
					 | 
				
			||||||
			Env + "=" + strconv.Itoa(fd),
 | 
					 | 
				
			||||||
			"FORTIFY_APP_ID=" + aid,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// format fsu supplementary groups
 | 
					 | 
				
			||||||
	if len(supp) > 0 {
 | 
					 | 
				
			||||||
		fmsg.Verbosef("attaching supplementary group ids %s", supp)
 | 
					 | 
				
			||||||
		s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
					 | 
				
			||||||
	s.cmd.Dir = "/"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// pass sync fd if set
 | 
					 | 
				
			||||||
	if syncFd != nil {
 | 
					 | 
				
			||||||
		fd := proc.ExtraFile(s.cmd, syncFd)
 | 
					 | 
				
			||||||
		s.sync = &fd
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
					 | 
				
			||||||
	// withhold messages to stderr
 | 
					 | 
				
			||||||
	fmsg.Suspend()
 | 
					 | 
				
			||||||
	if err := s.cmd.Start(); err != nil {
 | 
					 | 
				
			||||||
		return nil, fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
			"cannot start fsu:")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	startTime := time.Now().UTC()
 | 
					 | 
				
			||||||
	return &startTime, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
					 | 
				
			||||||
	// kill shim if something goes wrong and an error is returned
 | 
					 | 
				
			||||||
	s.killFallback = make(chan error, 1)
 | 
					 | 
				
			||||||
	killShim := func() {
 | 
					 | 
				
			||||||
		if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
 | 
					 | 
				
			||||||
			s.killFallback <- err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer func() { killShim() }()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	payload.Sync = s.sync
 | 
					 | 
				
			||||||
	encodeErr := make(chan error)
 | 
					 | 
				
			||||||
	go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	select {
 | 
					 | 
				
			||||||
	// encode return indicates setup completion
 | 
					 | 
				
			||||||
	case err := <-encodeErr:
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
				"cannot transmit shim config:")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		killShim = func() {}
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setup canceled before payload was accepted
 | 
					 | 
				
			||||||
	case <-ctx.Done():
 | 
					 | 
				
			||||||
		err := ctx.Err()
 | 
					 | 
				
			||||||
		if errors.Is(err, context.Canceled) {
 | 
					 | 
				
			||||||
			return fmsg.WrapError(errors.New("shim setup canceled"),
 | 
					 | 
				
			||||||
				"shim setup canceled")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, context.DeadlineExceeded) {
 | 
					 | 
				
			||||||
			return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
 | 
					 | 
				
			||||||
				"deadline exceeded waiting for shim")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// unreachable
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,23 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_SHIM"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
	// bwrap, target full exec path
 | 
					 | 
				
			||||||
	Exec [2]string
 | 
					 | 
				
			||||||
	// bwrap config
 | 
					 | 
				
			||||||
	Bwrap *bwrap.Config
 | 
					 | 
				
			||||||
	// path to outer home directory
 | 
					 | 
				
			||||||
	Home string
 | 
					 | 
				
			||||||
	// sync fd
 | 
					 | 
				
			||||||
	Sync *uintptr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -3,10 +3,15 @@ package internal
 | 
				
			|||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
					const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	Version = compPoison
 | 
						version = compPoison
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Check validates string value set at compile time.
 | 
					// check validates string value set at compile time.
 | 
				
			||||||
func Check(s string) (string, bool) {
 | 
					func check(s string) (string, bool) { return s, s != compPoison && s != "" }
 | 
				
			||||||
	return s, s != compPoison && s != ""
 | 
					
 | 
				
			||||||
 | 
					func Version() string {
 | 
				
			||||||
 | 
						if v, ok := check(version); ok {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "impure"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								internal/fmsg/msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/fmsg/msg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					package fmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Output struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (Output) IsVerbose() bool                         { return Load() }
 | 
				
			||||||
 | 
					func (Output) Verbose(v ...any)                        { Verbose(v...) }
 | 
				
			||||||
 | 
					func (Output) Verbosef(format string, v ...any)        { Verbosef(format, v...) }
 | 
				
			||||||
 | 
					func (Output) WrapErr(err error, a ...any) error       { return WrapError(err, a...) }
 | 
				
			||||||
 | 
					func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
 | 
				
			||||||
 | 
					func (Output) Suspend()                                { Suspend() }
 | 
				
			||||||
 | 
					func (Output) Resume() bool                            { return Resume() }
 | 
				
			||||||
 | 
					func (Output) BeforeExit()                             { BeforeExit() }
 | 
				
			||||||
							
								
								
									
										17
									
								
								internal/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								internal/output.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func InstallFmsg(verbose bool) {
 | 
				
			||||||
 | 
						fmsg.Store(verbose)
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						system.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						if verbose {
 | 
				
			||||||
 | 
							seccomp.SetOutput(fmsg.Verbose)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,11 +1,23 @@
 | 
				
			|||||||
package internal
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "path"
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	Fsu = compPoison
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Path(p string) (string, bool) {
 | 
					var (
 | 
				
			||||||
	return p, p != compPoison && p != "" && path.IsAbs(p)
 | 
						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 checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
package internal
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
 | 
					 | 
				
			||||||
	// linux/sched/coredump.h
 | 
					 | 
				
			||||||
	if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
 | 
					 | 
				
			||||||
		return errno
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func PR_SET_PDEATHSIG__SIGKILL() error {
 | 
					 | 
				
			||||||
	if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
 | 
					 | 
				
			||||||
		return errno
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			slices.Sort(aids)
 | 
								slices.Sort(aids)
 | 
				
			||||||
			want := []int{0, 1}
 | 
								want := []int{0, 1}
 | 
				
			||||||
			if slices.Compare(aids, want) != 0 {
 | 
								if !slices.Equal(aids, want) {
 | 
				
			||||||
				t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
									t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,10 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// State provides safe interaction with operating system state.
 | 
					// State provides safe interaction with operating system state.
 | 
				
			||||||
type State interface {
 | 
					type State interface {
 | 
				
			||||||
	// Geteuid provides [os.Geteuid].
 | 
						// Getuid provides [os.Getuid].
 | 
				
			||||||
	Geteuid() int
 | 
						Getuid() int
 | 
				
			||||||
 | 
						// Getgid provides [os.Getgid].
 | 
				
			||||||
 | 
						Getgid() int
 | 
				
			||||||
	// LookupEnv provides [os.LookupEnv].
 | 
						// LookupEnv provides [os.LookupEnv].
 | 
				
			||||||
	LookupEnv(key string) (string, bool)
 | 
						LookupEnv(key string) (string, bool)
 | 
				
			||||||
	// TempDir provides [os.TempDir].
 | 
						// TempDir provides [os.TempDir].
 | 
				
			||||||
@ -47,7 +49,7 @@ type State interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CopyPaths is a generic implementation of [System.Paths].
 | 
					// CopyPaths is a generic implementation of [System.Paths].
 | 
				
			||||||
func CopyPaths(os State, v *fst.Paths) {
 | 
					func CopyPaths(os State, v *fst.Paths) {
 | 
				
			||||||
	v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
 | 
						v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
						fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"os/user"
 | 
						"os/user"
 | 
				
			||||||
@ -16,6 +15,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Std implements System using the standard library.
 | 
					// Std implements System using the standard library.
 | 
				
			||||||
@ -31,11 +31,12 @@ type Std struct {
 | 
				
			|||||||
	uidMu sync.RWMutex
 | 
						uidMu sync.RWMutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Std) Geteuid() int                                 { return os.Geteuid() }
 | 
					func (s *Std) Getuid() int                                  { return os.Getuid() }
 | 
				
			||||||
 | 
					func (s *Std) Getgid() int                                  { return os.Getgid() }
 | 
				
			||||||
func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
					func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
				
			||||||
func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
					func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
				
			||||||
func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
					func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
				
			||||||
func (s *Std) MustExecutable() string                       { return internal.MustExecutable() }
 | 
					func (s *Std) MustExecutable() string                       { return sandbox.MustExecutable() }
 | 
				
			||||||
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
 | 
					func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
 | 
				
			||||||
func (s *Std) ReadDir(name string) ([]os.DirEntry, error)   { return os.ReadDir(name) }
 | 
					func (s *Std) ReadDir(name string) ([]os.DirEntry, error)   { return os.ReadDir(name) }
 | 
				
			||||||
func (s *Std) Stat(name string) (fs.FileInfo, error)        { return os.Stat(name) }
 | 
					func (s *Std) Stat(name string) (fs.FileInfo, error)        { return os.Stat(name) }
 | 
				
			||||||
@ -79,32 +80,27 @@ func (s *Std) Uid(aid int) (int, error) {
 | 
				
			|||||||
	defer func() { s.uidCopy[aid] = u }()
 | 
						defer func() { s.uidCopy[aid] = u }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u.uid = -1
 | 
						u.uid = -1
 | 
				
			||||||
	if fsu, ok := internal.Check(internal.Fsu); !ok {
 | 
						fsuPath := internal.MustFsuPath()
 | 
				
			||||||
		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 = "/"
 | 
					 | 
				
			||||||
		var (
 | 
					 | 
				
			||||||
			p         []byte
 | 
					 | 
				
			||||||
			exitError *exec.ExitError
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if p, u.err = cmd.Output(); u.err == nil {
 | 
						cmd := exec.Command(fsuPath)
 | 
				
			||||||
			u.uid, u.err = strconv.Atoi(string(p))
 | 
						cmd.Path = fsuPath
 | 
				
			||||||
			if u.err != nil {
 | 
						cmd.Stderr = os.Stderr // pass through fatal messages
 | 
				
			||||||
				u.err = fmsg.WrapErrorSuffix(u.err, "cannot parse uid from fsu:")
 | 
						cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
 | 
				
			||||||
			}
 | 
						cmd.Dir = "/"
 | 
				
			||||||
		} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
 | 
						var (
 | 
				
			||||||
			u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
 | 
							p         []byte
 | 
				
			||||||
		} else if os.IsNotExist(u.err) {
 | 
							exitError *exec.ExitError
 | 
				
			||||||
			u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsu))
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p, u.err = cmd.Output(); u.err == nil {
 | 
				
			||||||
 | 
							u.uid, u.err = strconv.Atoi(string(p))
 | 
				
			||||||
 | 
							if u.err != nil {
 | 
				
			||||||
 | 
								u.err = fmsg.WrapErrorSuffix(u.err, "cannot parse uid from fsu:")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return u.uid, u.err
 | 
						} 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))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return u.uid, u.err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										58
									
								
								ldd/exec.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								ldd/exec.go
									
									
									
									
									
								
							@ -3,56 +3,56 @@ package ldd
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const lddTimeout = 2 * time.Second
 | 
					const lddTimeout = 2 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
 | 
						msgStatic      = []byte("Not a valid dynamic program")
 | 
				
			||||||
	msgStaticGlibc = []byte("not a dynamic executable")
 | 
						msgStaticGlibc = []byte("not a dynamic executable")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Exec(ctx context.Context, p string) ([]*Entry, error) {
 | 
					func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
 | 
				
			||||||
	var h helper.Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if toolPath, err := exec.LookPath("ldd"); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	} else if h, err = helper.NewBwrap(
 | 
					 | 
				
			||||||
		(&bwrap.Config{
 | 
					 | 
				
			||||||
			Hostname:      "fortify-ldd",
 | 
					 | 
				
			||||||
			Chdir:         "/",
 | 
					 | 
				
			||||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
		}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
 | 
					 | 
				
			||||||
		nil, func(_, _ int) []string { return []string{p} },
 | 
					 | 
				
			||||||
		nil, nil,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
 | 
					 | 
				
			||||||
	h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ExecFilter(ctx context.Context,
 | 
				
			||||||
 | 
						commandContext func(context.Context) *exec.Cmd,
 | 
				
			||||||
 | 
						f func([]byte) []byte,
 | 
				
			||||||
 | 
						p string) ([]*Entry, error) {
 | 
				
			||||||
	c, cancel := context.WithTimeout(ctx, lddTimeout)
 | 
						c, cancel := context.WithTimeout(ctx, lddTimeout)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
	if err := h.Start(c, false); err != nil {
 | 
						container := sandbox.New(c, "ldd", p)
 | 
				
			||||||
 | 
						container.CommandContext = commandContext
 | 
				
			||||||
 | 
						container.Hostname = "fortify-ldd"
 | 
				
			||||||
 | 
						stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
 | 
				
			||||||
 | 
						container.Stdout = stdout
 | 
				
			||||||
 | 
						container.Stderr = stderr
 | 
				
			||||||
 | 
						container.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := container.Start(); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := h.Wait(); err != nil {
 | 
						defer func() { _, _ = io.Copy(os.Stderr, stderr) }()
 | 
				
			||||||
 | 
						if err := container.Serve(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Wait(); err != nil {
 | 
				
			||||||
		m := stderr.Bytes()
 | 
							m := stderr.Bytes()
 | 
				
			||||||
		if bytes.Contains(m, msgStaticGlibc) {
 | 
							if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) ||
 | 
				
			||||||
 | 
								bytes.Contains(m, msgStaticGlibc) {
 | 
				
			||||||
			return nil, nil
 | 
								return nil, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		_, _ = os.Stderr.Write(m)
 | 
					 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return Parse(stdout)
 | 
						v := stdout.Bytes()
 | 
				
			||||||
 | 
						if f != nil {
 | 
				
			||||||
 | 
							v = f(v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Parse(v)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
package ldd
 | 
					package ldd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@ -15,8 +14,8 @@ type Entry struct {
 | 
				
			|||||||
	Location uint64 `json:"location"`
 | 
						Location uint64 `json:"location"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Parse(stdout fmt.Stringer) ([]*Entry, error) {
 | 
					func Parse(p []byte) ([]*Entry, error) {
 | 
				
			||||||
	payload := strings.Split(strings.TrimSpace(stdout.String()), "\n")
 | 
						payload := strings.Split(strings.TrimSpace(string(p)), "\n")
 | 
				
			||||||
	result := make([]*Entry, len(payload))
 | 
						result := make([]*Entry, len(payload))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i, ent := range payload {
 | 
						for i, ent := range payload {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ package ldd_test
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
@ -34,10 +33,7 @@ libzstd.so.1 => /usr/lib/libzstd.so.1 7ff71bfd2000
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			stdout := new(strings.Builder)
 | 
								if _, err := ldd.Parse([]byte(tc.out)); !errors.Is(err, tc.wantErr) {
 | 
				
			||||||
			stdout.WriteString(tc.out)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if _, err := ldd.Parse(stdout); !errors.Is(err, tc.wantErr) {
 | 
					 | 
				
			||||||
				t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
 | 
									t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@ -111,10 +107,7 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tc.file, func(t *testing.T) {
 | 
							t.Run(tc.file, func(t *testing.T) {
 | 
				
			||||||
			stdout := new(strings.Builder)
 | 
								if got, err := ldd.Parse([]byte(tc.out)); err != nil {
 | 
				
			||||||
			stdout.WriteString(tc.out)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if got, err := ldd.Parse(stdout); err != nil {
 | 
					 | 
				
			||||||
				t.Errorf("Parse() error = %v", err)
 | 
									t.Errorf("Parse() error = %v", err)
 | 
				
			||||||
			} else if !reflect.DeepEqual(got, tc.want) {
 | 
								} else if !reflect.DeepEqual(got, tc.want) {
 | 
				
			||||||
				t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
 | 
									t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								ldd/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ldd/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package ldd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Path returns a deterministic, deduplicated slice of absolute directory paths in entries.
 | 
				
			||||||
 | 
					func Path(entries []*Entry) []string {
 | 
				
			||||||
 | 
						p := make([]string, 0, len(entries)*2)
 | 
				
			||||||
 | 
						for _, entry := range entries {
 | 
				
			||||||
 | 
							if path.IsAbs(entry.Path) {
 | 
				
			||||||
 | 
								p = append(p, path.Dir(entry.Path))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if path.IsAbs(entry.Name) {
 | 
				
			||||||
 | 
								p = append(p, path.Dir(entry.Name))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						slices.Sort(p)
 | 
				
			||||||
 | 
						return slices.Compact(p)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										110
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										110
									
								
								main.go
									
									
									
									
									
								
							@ -18,14 +18,12 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
						"git.gensokyo.uk/security/fortify/command"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	init0 "git.gensokyo.uk/security/fortify/internal/app/init"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,10 +39,10 @@ func init() { fmsg.Prepare("fortify") }
 | 
				
			|||||||
var std sys.State = new(sys.Std)
 | 
					var std sys.State = new(sys.Std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
	init0.TryArgv0()
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
		// not fatal: this program runs as the privileged user
 | 
							// not fatal: this program runs as the privileged user
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -68,10 +66,15 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		flagVerbose bool
 | 
							flagVerbose bool
 | 
				
			||||||
		flagJSON    bool
 | 
							flagJSON    bool
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
 | 
						c := command.New(out, log.Printf, "fortify", func([]string) error {
 | 
				
			||||||
 | 
							internal.InstallFmsg(flagVerbose)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}).
 | 
				
			||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
							Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
						c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
				
			||||||
		if len(args) < 1 {
 | 
							if len(args) < 1 {
 | 
				
			||||||
			log.Fatal("app requires at least 1 argument")
 | 
								log.Fatal("app requires at least 1 argument")
 | 
				
			||||||
@ -79,10 +82,9 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// config extraArgs...
 | 
							// config extraArgs...
 | 
				
			||||||
		config := tryPath(args[0])
 | 
							config := tryPath(args[0])
 | 
				
			||||||
		config.Command = append(config.Command, args[1:]...)
 | 
							config.Args = append(config.Args, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// invoke app
 | 
							runApp(config)
 | 
				
			||||||
		runApp(app.MustNew(std), config)
 | 
					 | 
				
			||||||
		panic("unreachable")
 | 
							panic("unreachable")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -93,19 +95,20 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			mpris             bool
 | 
								mpris             bool
 | 
				
			||||||
			dbusVerbose       bool
 | 
								dbusVerbose       bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			fid         string
 | 
								fid      string
 | 
				
			||||||
			aid         int
 | 
								aid      int
 | 
				
			||||||
			groups      command.RepeatableFlag
 | 
								groups   command.RepeatableFlag
 | 
				
			||||||
			homeDir     string
 | 
								homeDir  string
 | 
				
			||||||
			userName    string
 | 
								userName string
 | 
				
			||||||
			enablements [system.ELen]bool
 | 
					
 | 
				
			||||||
 | 
								wayland, x11, dBus, pulse bool
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
							c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
				
			||||||
			// initialise config from flags
 | 
								// initialise config from flags
 | 
				
			||||||
			config := &fst.Config{
 | 
								config := &fst.Config{
 | 
				
			||||||
				ID:      fid,
 | 
									ID:   fid,
 | 
				
			||||||
				Command: args,
 | 
									Args: args,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if aid < 0 || aid > 9999 {
 | 
								if aid < 0 || aid > 9999 {
 | 
				
			||||||
@ -155,15 +158,21 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			config.Confinement.Outer = homeDir
 | 
								config.Confinement.Outer = homeDir
 | 
				
			||||||
			config.Confinement.Username = userName
 | 
								config.Confinement.Username = userName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// enablements from flags
 | 
								if wayland {
 | 
				
			||||||
			for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
 | 
									config.Confinement.Enablements |= system.EWayland
 | 
				
			||||||
				if enablements[i] {
 | 
								}
 | 
				
			||||||
					config.Confinement.Enablements.Set(i)
 | 
								if x11 {
 | 
				
			||||||
				}
 | 
									config.Confinement.Enablements |= system.EX11
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if dBus {
 | 
				
			||||||
 | 
									config.Confinement.Enablements |= system.EDBus
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if pulse {
 | 
				
			||||||
 | 
									config.Confinement.Enablements |= system.EPulse
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// parse D-Bus config file from flags if applicable
 | 
								// parse D-Bus config file from flags if applicable
 | 
				
			||||||
			if enablements[system.EDBus] {
 | 
								if dBus {
 | 
				
			||||||
				if dbusConfigSession == "builtin" {
 | 
									if dbusConfigSession == "builtin" {
 | 
				
			||||||
					config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
 | 
										config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
@ -191,7 +200,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// invoke app
 | 
								// invoke app
 | 
				
			||||||
			runApp(app.MustNew(std), config)
 | 
								runApp(config)
 | 
				
			||||||
			panic("unreachable")
 | 
								panic("unreachable")
 | 
				
			||||||
		}).
 | 
							}).
 | 
				
			||||||
			Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
								Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
				
			||||||
@ -212,13 +221,13 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
				"Application home directory").
 | 
									"Application home directory").
 | 
				
			||||||
			Flag(&userName, "u", command.StringFlag("chronos"),
 | 
								Flag(&userName, "u", command.StringFlag("chronos"),
 | 
				
			||||||
				"Passwd name within sandbox").
 | 
									"Passwd name within sandbox").
 | 
				
			||||||
			Flag(&enablements[system.EWayland], "wayland", command.BoolFlag(false),
 | 
								Flag(&wayland, "wayland", command.BoolFlag(false),
 | 
				
			||||||
				"Allow Wayland connections").
 | 
									"Allow Wayland connections").
 | 
				
			||||||
			Flag(&enablements[system.EX11], "X", command.BoolFlag(false),
 | 
								Flag(&x11, "X", command.BoolFlag(false),
 | 
				
			||||||
				"Share X11 socket and allow connection").
 | 
									"Share X11 socket and allow connection").
 | 
				
			||||||
			Flag(&enablements[system.EDBus], "dbus", command.BoolFlag(false),
 | 
								Flag(&dBus, "dbus", command.BoolFlag(false),
 | 
				
			||||||
				"Proxy D-Bus connection").
 | 
									"Proxy D-Bus connection").
 | 
				
			||||||
			Flag(&enablements[system.EPulse], "pulse", command.BoolFlag(false),
 | 
								Flag(&pulse, "pulse", command.BoolFlag(false),
 | 
				
			||||||
				"Share PulseAudio socket and cookie")
 | 
									"Share PulseAudio socket and cookie")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -249,11 +258,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
	}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
 | 
						}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Command("version", "Show fortify version", func(args []string) error {
 | 
						c.Command("version", "Show fortify version", func(args []string) error {
 | 
				
			||||||
		if v, ok := internal.Check(internal.Version); ok {
 | 
							fmt.Println(internal.Version())
 | 
				
			||||||
			fmt.Println(v)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			fmt.Println("impure")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return errSuccess
 | 
							return errSuccess
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -272,45 +277,22 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		return errSuccess
 | 
							return errSuccess
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// internal commands
 | 
					 | 
				
			||||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
					 | 
				
			||||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c
 | 
						return c
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func runApp(a fst.App, config *fst.Config) {
 | 
					func runApp(config *fst.Config) {
 | 
				
			||||||
	rs := new(fst.RunState)
 | 
					 | 
				
			||||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
						ctx, stop := signal.NotifyContext(context.Background(),
 | 
				
			||||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
							syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
	defer stop() // unreachable
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
						a := app.MustNew(ctx, std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if fmsg.Load() {
 | 
						rs := new(fst.RunState)
 | 
				
			||||||
		seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if sa, err := a.Seal(config); err != nil {
 | 
						if sa, err := a.Seal(config); err != nil {
 | 
				
			||||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
							fmsg.PrintBaseError(err, "cannot seal app:")
 | 
				
			||||||
		internal.Exit(1)
 | 
							rs.ExitCode = 1
 | 
				
			||||||
	} else if err = sa.Run(ctx, rs); err != nil {
 | 
						} else {
 | 
				
			||||||
		if rs.Time == nil {
 | 
							// this updates ExitCode
 | 
				
			||||||
			fmsg.PrintBaseError(err, "cannot start app:")
 | 
							app.PrintRunStateErr(rs, sa.Run(rs))
 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			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)
 | 
						internal.Exit(rs.ExitCode)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										72
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								nixos.nix
									
									
									
									
									
								
							@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					packages:
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
  pkgs,
 | 
					  pkgs,
 | 
				
			||||||
@ -26,7 +27,7 @@ let
 | 
				
			|||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  imports = [ ./options.nix ];
 | 
					  imports = [ (import ./options.nix packages) ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config = mkIf cfg.enable {
 | 
					  config = mkIf cfg.enable {
 | 
				
			||||||
    security.wrappers.fsu = {
 | 
					    security.wrappers.fsu = {
 | 
				
			||||||
@ -77,29 +78,22 @@ in
 | 
				
			|||||||
                      };
 | 
					                      };
 | 
				
			||||||
                    in
 | 
					                    in
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                      session_bus =
 | 
					                      session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault app.id)) else (extendDBusDefault app.id default);
 | 
				
			||||||
                        if app.dbus.session != null then
 | 
					 | 
				
			||||||
                          (app.dbus.session (extendDBusDefault app.id))
 | 
					 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                          (extendDBusDefault app.id default);
 | 
					 | 
				
			||||||
                      system_bus = app.dbus.system;
 | 
					                      system_bus = app.dbus.system;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                  command = if app.command == null then app.name else app.command;
 | 
					                  command = if app.command == null then app.name else app.command;
 | 
				
			||||||
                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
					                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
				
			||||||
                  enablements =
 | 
					                  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);
 | 
				
			||||||
                    with app.capability;
 | 
					                  isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11;
 | 
				
			||||||
                    (if wayland then 1 else 0)
 | 
					
 | 
				
			||||||
                    + (if x11 then 2 else 0)
 | 
					 | 
				
			||||||
                    + (if dbus then 4 else 0)
 | 
					 | 
				
			||||||
                    + (if pulse then 8 else 0);
 | 
					 | 
				
			||||||
                  conf = {
 | 
					                  conf = {
 | 
				
			||||||
                    inherit (app) id;
 | 
					                    inherit (app) id;
 | 
				
			||||||
                    command = [
 | 
					                    path = pkgs.writeScript "${app.name}-start" ''
 | 
				
			||||||
                      (pkgs.writeScript "${app.name}-start" ''
 | 
					                      #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
				
			||||||
                        #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
					                      ${script}
 | 
				
			||||||
                        ${script}
 | 
					                    '';
 | 
				
			||||||
                      '')
 | 
					                    args = [ "${app.name}-start" ];
 | 
				
			||||||
                    ];
 | 
					
 | 
				
			||||||
                    confinement = {
 | 
					                    confinement = {
 | 
				
			||||||
                      app_id = aid;
 | 
					                      app_id = aid;
 | 
				
			||||||
                      inherit (app) groups;
 | 
					                      inherit (app) groups;
 | 
				
			||||||
@ -107,18 +101,17 @@ in
 | 
				
			|||||||
                      home = getsubhome fid aid;
 | 
					                      home = getsubhome fid aid;
 | 
				
			||||||
                      sandbox = {
 | 
					                      sandbox = {
 | 
				
			||||||
                        inherit (app)
 | 
					                        inherit (app)
 | 
				
			||||||
 | 
					                          devel
 | 
				
			||||||
                          userns
 | 
					                          userns
 | 
				
			||||||
                          net
 | 
					                          net
 | 
				
			||||||
                          dev
 | 
					                          dev
 | 
				
			||||||
 | 
					                          tty
 | 
				
			||||||
 | 
					                          multiarch
 | 
				
			||||||
                          env
 | 
					                          env
 | 
				
			||||||
                          ;
 | 
					                          ;
 | 
				
			||||||
                        syscall = {
 | 
					 | 
				
			||||||
                          inherit (app) compat multiarch bluetooth;
 | 
					 | 
				
			||||||
                          deny_devel = !app.devel;
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                        map_real_uid = app.mapRealUid;
 | 
					                        map_real_uid = app.mapRealUid;
 | 
				
			||||||
                        no_new_session = app.tty;
 | 
					 | 
				
			||||||
                        direct_wayland = app.insecureWayland;
 | 
					                        direct_wayland = app.insecureWayland;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        filesystem =
 | 
					                        filesystem =
 | 
				
			||||||
                          let
 | 
					                          let
 | 
				
			||||||
                            bind = src: { inherit src; };
 | 
					                            bind = src: { inherit src; };
 | 
				
			||||||
@ -135,7 +128,6 @@ in
 | 
				
			|||||||
                            (mustBind "/bin")
 | 
					                            (mustBind "/bin")
 | 
				
			||||||
                            (mustBind "/usr/bin")
 | 
					                            (mustBind "/usr/bin")
 | 
				
			||||||
                            (mustBind "/nix/store")
 | 
					                            (mustBind "/nix/store")
 | 
				
			||||||
                            (mustBind "/run/current-system")
 | 
					 | 
				
			||||||
                            (bind "/sys/block")
 | 
					                            (bind "/sys/block")
 | 
				
			||||||
                            (bind "/sys/bus")
 | 
					                            (bind "/sys/bus")
 | 
				
			||||||
                            (bind "/sys/class")
 | 
					                            (bind "/sys/class")
 | 
				
			||||||
@ -146,8 +138,7 @@ in
 | 
				
			|||||||
                            (mustBind "/nix/var")
 | 
					                            (mustBind "/nix/var")
 | 
				
			||||||
                            (bind "/var/db/nix-channels")
 | 
					                            (bind "/var/db/nix-channels")
 | 
				
			||||||
                          ]
 | 
					                          ]
 | 
				
			||||||
                          ++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
 | 
					                          ++ optionals isGraphical [
 | 
				
			||||||
                            (bind "/run/opengl-driver")
 | 
					 | 
				
			||||||
                            (devBind "/dev/dri")
 | 
					                            (devBind "/dev/dri")
 | 
				
			||||||
                            (devBind "/dev/nvidiactl")
 | 
					                            (devBind "/dev/nvidiactl")
 | 
				
			||||||
                            (devBind "/dev/nvidia-modeset")
 | 
					                            (devBind "/dev/nvidia-modeset")
 | 
				
			||||||
@ -157,17 +148,38 @@ in
 | 
				
			|||||||
                          ]
 | 
					                          ]
 | 
				
			||||||
                          ++ app.extraPaths;
 | 
					                          ++ app.extraPaths;
 | 
				
			||||||
                        auto_etc = true;
 | 
					                        auto_etc = true;
 | 
				
			||||||
                        override = [ "/var/run/nscd" ];
 | 
					                        cover = [ "/var/run/nscd" ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        symlink =
 | 
				
			||||||
 | 
					                          [
 | 
				
			||||||
 | 
					                            [
 | 
				
			||||||
 | 
					                              "*/run/current-system"
 | 
				
			||||||
 | 
					                              "/run/current-system"
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                          ]
 | 
				
			||||||
 | 
					                          ++ optionals (isGraphical && config.hardware.graphics.enable) (
 | 
				
			||||||
 | 
					                            [
 | 
				
			||||||
 | 
					                              [
 | 
				
			||||||
 | 
					                                config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
 | 
				
			||||||
 | 
					                                "/run/opengl-driver"
 | 
				
			||||||
 | 
					                              ]
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                            ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
 | 
				
			||||||
 | 
					                              [
 | 
				
			||||||
 | 
					                                config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
 | 
				
			||||||
 | 
					                                /run/opengl-driver-32
 | 
				
			||||||
 | 
					                              ]
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
                      };
 | 
					                      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      inherit enablements;
 | 
					                      inherit enablements;
 | 
				
			||||||
                      inherit (dbusConfig) session_bus system_bus;
 | 
					                      inherit (dbusConfig) session_bus system_bus;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
                pkgs.writeShellScriptBin app.name ''
 | 
					                pkgs.writeShellScriptBin app.name ''
 | 
				
			||||||
                  exec fortify${
 | 
					                  exec fortify${if app.verbose then " -v" else ""} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
				
			||||||
                    if app.verbose then " -v" else ""
 | 
					 | 
				
			||||||
                  } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
					 | 
				
			||||||
                ''
 | 
					                ''
 | 
				
			||||||
              ) cfg.apps;
 | 
					              ) cfg.apps;
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								options.nix
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								options.nix
									
									
									
									
									
								
							@ -1,10 +1,8 @@
 | 
				
			|||||||
 | 
					packages:
 | 
				
			||||||
{ lib, pkgs, ... }:
 | 
					{ lib, pkgs, ... }:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let
 | 
					let
 | 
				
			||||||
  inherit (lib) types mkOption mkEnableOption;
 | 
					  inherit (lib) types mkOption mkEnableOption;
 | 
				
			||||||
  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					 | 
				
			||||||
    inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -14,13 +12,13 @@ in
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      package = mkOption {
 | 
					      package = mkOption {
 | 
				
			||||||
        type = types.package;
 | 
					        type = types.package;
 | 
				
			||||||
        default = fortify;
 | 
					        default = packages.${pkgs.system}.fortify;
 | 
				
			||||||
        description = "The fortify package to use.";
 | 
					        description = "The fortify package to use.";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      fsuPackage = mkOption {
 | 
					      fsuPackage = mkOption {
 | 
				
			||||||
        type = types.package;
 | 
					        type = types.package;
 | 
				
			||||||
        default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
 | 
					        default = packages.${pkgs.system}.fsu;
 | 
				
			||||||
        description = "The fsu package to use.";
 | 
					        description = "The fsu package to use.";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,21 +148,19 @@ in
 | 
				
			|||||||
                '';
 | 
					                '';
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              nix = mkEnableOption "nix daemon";
 | 
					              devel = mkEnableOption "debugging-related kernel interfaces";
 | 
				
			||||||
              userns = mkEnableOption "user namespace";
 | 
					              userns = mkEnableOption "user namespace creation";
 | 
				
			||||||
              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
					 | 
				
			||||||
              dev = mkEnableOption "access to all devices";
 | 
					 | 
				
			||||||
              tty = mkEnableOption "access to the controlling terminal";
 | 
					              tty = mkEnableOption "access to the controlling terminal";
 | 
				
			||||||
              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
					              multiarch = mkEnableOption "multiarch kernel-level support";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              net = mkEnableOption "network access" // {
 | 
					              net = mkEnableOption "network access" // {
 | 
				
			||||||
                default = true;
 | 
					                default = true;
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              compat = mkEnableOption "disable syscall filter extensions";
 | 
					              nix = mkEnableOption "nix daemon access";
 | 
				
			||||||
              devel = mkEnableOption "development kernel APIs";
 | 
					              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
				
			||||||
              multiarch = mkEnableOption "multiarch kernel support";
 | 
					              dev = mkEnableOption "access to all devices";
 | 
				
			||||||
              bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
 | 
					              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              gpu = mkOption {
 | 
					              gpu = mkOption {
 | 
				
			||||||
                type = nullOr bool;
 | 
					                type = nullOr bool;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										75
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								package.nix
									
									
									
									
									
								
							@ -4,7 +4,6 @@
 | 
				
			|||||||
  buildGoModule,
 | 
					  buildGoModule,
 | 
				
			||||||
  makeBinaryWrapper,
 | 
					  makeBinaryWrapper,
 | 
				
			||||||
  xdg-dbus-proxy,
 | 
					  xdg-dbus-proxy,
 | 
				
			||||||
  bubblewrap,
 | 
					 | 
				
			||||||
  pkg-config,
 | 
					  pkg-config,
 | 
				
			||||||
  libffi,
 | 
					  libffi,
 | 
				
			||||||
  libseccomp,
 | 
					  libseccomp,
 | 
				
			||||||
@ -14,6 +13,18 @@
 | 
				
			|||||||
  wayland-scanner,
 | 
					  wayland-scanner,
 | 
				
			||||||
  xorg,
 | 
					  xorg,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # for fpkg
 | 
				
			||||||
 | 
					  zstd,
 | 
				
			||||||
 | 
					  gnutar,
 | 
				
			||||||
 | 
					  coreutils,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # for passthru.buildInputs
 | 
				
			||||||
 | 
					  go,
 | 
				
			||||||
 | 
					  gcc,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # for check
 | 
				
			||||||
 | 
					  util-linux,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  glibc, # for ldd
 | 
					  glibc, # for ldd
 | 
				
			||||||
  withStatic ? stdenv.hostPlatform.isStatic,
 | 
					  withStatic ? stdenv.hostPlatform.isStatic,
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
@ -25,10 +36,7 @@ buildGoModule rec {
 | 
				
			|||||||
  src = builtins.path {
 | 
					  src = builtins.path {
 | 
				
			||||||
    name = "${pname}-src";
 | 
					    name = "${pname}-src";
 | 
				
			||||||
    path = lib.cleanSource ./.;
 | 
					    path = lib.cleanSource ./.;
 | 
				
			||||||
    filter =
 | 
					    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
				
			||||||
      path: type:
 | 
					 | 
				
			||||||
      !(type == "regular" && lib.hasSuffix ".nix" path)
 | 
					 | 
				
			||||||
      && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  vendorHash = null;
 | 
					  vendorHash = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,17 +47,15 @@ buildGoModule rec {
 | 
				
			|||||||
        ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
 | 
					        ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      (
 | 
					      (
 | 
				
			||||||
        [
 | 
					        [ "-s -w" ]
 | 
				
			||||||
          "-s -w"
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        ++ lib.optionals withStatic [
 | 
					        ++ lib.optionals withStatic [
 | 
				
			||||||
          "-linkmode external"
 | 
					          "-linkmode external"
 | 
				
			||||||
          "-extldflags \"-static\""
 | 
					          "-extldflags \"-static\""
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        Version = "v${version}";
 | 
					        version = "v${version}";
 | 
				
			||||||
        Fsu = "/run/wrappers/bin/fsu";
 | 
					        fsu = "/run/wrappers/bin/fsu";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # nix build environment does not allow acls
 | 
					  # nix build environment does not allow acls
 | 
				
			||||||
@ -79,19 +85,42 @@ buildGoModule rec {
 | 
				
			|||||||
    HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
 | 
					    HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  postInstall = ''
 | 
					  postInstall =
 | 
				
			||||||
    install -D --target-directory=$out/share/zsh/site-functions comp/*
 | 
					    let
 | 
				
			||||||
 | 
					      appPackages = [
 | 
				
			||||||
 | 
					        glibc
 | 
				
			||||||
 | 
					        xdg-dbus-proxy
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    in
 | 
				
			||||||
 | 
					    ''
 | 
				
			||||||
 | 
					      install -D --target-directory=$out/share/zsh/site-functions comp/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mkdir "$out/libexec"
 | 
					      mkdir "$out/libexec"
 | 
				
			||||||
    mv "$out"/bin/* "$out/libexec/"
 | 
					      mv "$out"/bin/* "$out/libexec/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
 | 
					      makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
 | 
				
			||||||
      --inherit-argv0 --prefix PATH : ${
 | 
					        --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
 | 
				
			||||||
        lib.makeBinPath [
 | 
					
 | 
				
			||||||
          glibc
 | 
					      makeBinaryWrapper "$out/libexec/fpkg" "$out/bin/fpkg" \
 | 
				
			||||||
          bubblewrap
 | 
					        --inherit-argv0 --prefix PATH : ${
 | 
				
			||||||
          xdg-dbus-proxy
 | 
					          lib.makeBinPath (
 | 
				
			||||||
        ]
 | 
					            appPackages
 | 
				
			||||||
      }
 | 
					            ++ [
 | 
				
			||||||
  '';
 | 
					              zstd
 | 
				
			||||||
 | 
					              gnutar
 | 
				
			||||||
 | 
					              coreutils
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  passthru.targetPkgs =
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      go
 | 
				
			||||||
 | 
					      gcc
 | 
				
			||||||
 | 
					      xorg.xorgproto
 | 
				
			||||||
 | 
					      util-linux
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    ++ buildInputs
 | 
				
			||||||
 | 
					    ++ nativeBuildInputs;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								parse.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								parse.go
									
									
									
									
									
								
							@ -50,9 +50,12 @@ func tryPath(name string) (config *fst.Config) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func tryFd(name string) io.ReadCloser {
 | 
					func tryFd(name string) io.ReadCloser {
 | 
				
			||||||
	if v, err := strconv.Atoi(name); err != nil {
 | 
						if v, err := strconv.Atoi(name); err != nil {
 | 
				
			||||||
		fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
							if !errors.Is(err, strconv.ErrSyntax) {
 | 
				
			||||||
 | 
								fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							fmsg.Verbosef("trying config stream from %d", v)
 | 
				
			||||||
		fd := uintptr(v)
 | 
							fd := uintptr(v)
 | 
				
			||||||
		if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
 | 
							if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
 | 
				
			||||||
			if errors.Is(errno, syscall.EBADF) {
 | 
								if errors.Is(errno, syscall.EBADF) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								print.go
									
									
									
									
									
								
							@ -89,10 +89,10 @@ func printShowInstance(
 | 
				
			|||||||
				flags = append(flags, name)
 | 
									flags = append(flags, name)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		writeFlag("userns", sandbox.UserNS)
 | 
							writeFlag("userns", sandbox.Userns)
 | 
				
			||||||
		writeFlag("net", sandbox.Net)
 | 
							writeFlag("net", sandbox.Net)
 | 
				
			||||||
		writeFlag("dev", sandbox.Dev)
 | 
							writeFlag("dev", sandbox.Dev)
 | 
				
			||||||
		writeFlag("tty", sandbox.NoNewSession)
 | 
							writeFlag("tty", sandbox.Tty)
 | 
				
			||||||
		writeFlag("mapuid", sandbox.MapRealUID)
 | 
							writeFlag("mapuid", sandbox.MapRealUID)
 | 
				
			||||||
		writeFlag("directwl", sandbox.DirectWayland)
 | 
							writeFlag("directwl", sandbox.DirectWayland)
 | 
				
			||||||
		writeFlag("autoetc", sandbox.AutoEtc)
 | 
							writeFlag("autoetc", sandbox.AutoEtc)
 | 
				
			||||||
@ -107,14 +107,14 @@ func printShowInstance(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		t.Printf(" Etc:\t%s\n", etc)
 | 
							t.Printf(" Etc:\t%s\n", etc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if len(sandbox.Override) > 0 {
 | 
							if len(sandbox.Cover) > 0 {
 | 
				
			||||||
			t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
 | 
								t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Env           map[string]string   `json:"env"`
 | 
							// Env           map[string]string   `json:"env"`
 | 
				
			||||||
		// Link          [][2]string         `json:"symlink"`
 | 
							// Link          [][2]string         `json:"symlink"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
 | 
						t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
 | 
				
			||||||
	t.Printf("\n")
 | 
						t.Printf("\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !short {
 | 
						if !short {
 | 
				
			||||||
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
 | 
				
			|||||||
		)
 | 
							)
 | 
				
			||||||
		if e.Config != nil {
 | 
							if e.Config != nil {
 | 
				
			||||||
			es = e.Config.Confinement.Enablements.String()
 | 
								es = e.Config.Confinement.Enablements.String()
 | 
				
			||||||
			cs = fmt.Sprintf("%q", e.Config.Command)
 | 
								cs = fmt.Sprintf("%q", e.Config.Args)
 | 
				
			||||||
			as = strconv.Itoa(e.Config.Confinement.AppID)
 | 
								as = strconv.Itoa(e.Config.Confinement.AppID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
 | 
							t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
 | 
				
			||||||
 | 
				
			|||||||
@ -37,13 +37,13 @@ func Test_printShowInstance(t *testing.T) {
 | 
				
			|||||||
	}{
 | 
						}{
 | 
				
			||||||
		{"config", nil, fst.Template(), false, false, `App
 | 
							{"config", nil, fst.Template(), false, false, `App
 | 
				
			||||||
 ID:             9 (org.chromium.Chromium)
 | 
					 ID:             9 (org.chromium.Chromium)
 | 
				
			||||||
 Enablements:    Wayland, D-Bus, PulseAudio
 | 
					 Enablements:    wayland, dbus, pulseaudio
 | 
				
			||||||
 Groups:         ["video"]
 | 
					 Groups:         ["video"]
 | 
				
			||||||
 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
					 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
				
			||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -74,14 +74,14 @@ System bus
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
		{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
 | 
							{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Flags:          none
 | 
					 Flags:          none
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
@ -90,7 +90,7 @@ App
 | 
				
			|||||||
`},
 | 
					`},
 | 
				
			||||||
		{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
 | 
							{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Flags:          none
 | 
					 Flags:          none
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
@ -105,7 +105,7 @@ Extra ACL
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -121,13 +121,13 @@ Session bus
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             9 (org.chromium.Chromium)
 | 
					 ID:             9 (org.chromium.Chromium)
 | 
				
			||||||
 Enablements:    Wayland, D-Bus, PulseAudio
 | 
					 Enablements:    wayland, dbus, pulseaudio
 | 
				
			||||||
 Groups:         ["video"]
 | 
					 Groups:         ["video"]
 | 
				
			||||||
 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
					 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
				
			||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -162,7 +162,7 @@ State
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -192,7 +192,8 @@ App
 | 
				
			|||||||
  "pid": 3735928559,
 | 
					  "pid": 3735928559,
 | 
				
			||||||
  "config": {
 | 
					  "config": {
 | 
				
			||||||
    "id": "org.chromium.Chromium",
 | 
					    "id": "org.chromium.Chromium",
 | 
				
			||||||
    "command": [
 | 
					    "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					    "args": [
 | 
				
			||||||
      "chromium",
 | 
					      "chromium",
 | 
				
			||||||
      "--ignore-gpu-blocklist",
 | 
					      "--ignore-gpu-blocklist",
 | 
				
			||||||
      "--disable-smooth-scrolling",
 | 
					      "--disable-smooth-scrolling",
 | 
				
			||||||
@ -209,24 +210,19 @@ App
 | 
				
			|||||||
      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
      "sandbox": {
 | 
					      "sandbox": {
 | 
				
			||||||
        "hostname": "localhost",
 | 
					        "hostname": "localhost",
 | 
				
			||||||
 | 
					        "seccomp": 32,
 | 
				
			||||||
 | 
					        "devel": true,
 | 
				
			||||||
        "userns": true,
 | 
					        "userns": true,
 | 
				
			||||||
        "net": true,
 | 
					        "net": true,
 | 
				
			||||||
        "dev": true,
 | 
					        "tty": true,
 | 
				
			||||||
        "syscall": {
 | 
					        "multiarch": true,
 | 
				
			||||||
          "compat": false,
 | 
					 | 
				
			||||||
          "deny_devel": true,
 | 
					 | 
				
			||||||
          "multiarch": true,
 | 
					 | 
				
			||||||
          "linux32": false,
 | 
					 | 
				
			||||||
          "can": false,
 | 
					 | 
				
			||||||
          "bluetooth": false
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "no_new_session": true,
 | 
					 | 
				
			||||||
        "map_real_uid": true,
 | 
					 | 
				
			||||||
        "env": {
 | 
					        "env": {
 | 
				
			||||||
          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "map_real_uid": true,
 | 
				
			||||||
 | 
					        "dev": true,
 | 
				
			||||||
        "filesystem": [
 | 
					        "filesystem": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "src": "/nix/store"
 | 
					            "src": "/nix/store"
 | 
				
			||||||
@ -259,7 +255,7 @@ App
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "etc": "/etc",
 | 
					        "etc": "/etc",
 | 
				
			||||||
        "auto_etc": true,
 | 
					        "auto_etc": true,
 | 
				
			||||||
        "override": [
 | 
					        "cover": [
 | 
				
			||||||
          "/var/run/nscd"
 | 
					          "/var/run/nscd"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -320,7 +316,8 @@ App
 | 
				
			|||||||
`},
 | 
					`},
 | 
				
			||||||
		{"json config", nil, fst.Template(), false, true, `{
 | 
							{"json config", nil, fst.Template(), false, true, `{
 | 
				
			||||||
  "id": "org.chromium.Chromium",
 | 
					  "id": "org.chromium.Chromium",
 | 
				
			||||||
  "command": [
 | 
					  "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					  "args": [
 | 
				
			||||||
    "chromium",
 | 
					    "chromium",
 | 
				
			||||||
    "--ignore-gpu-blocklist",
 | 
					    "--ignore-gpu-blocklist",
 | 
				
			||||||
    "--disable-smooth-scrolling",
 | 
					    "--disable-smooth-scrolling",
 | 
				
			||||||
@ -337,24 +334,19 @@ App
 | 
				
			|||||||
    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
    "sandbox": {
 | 
					    "sandbox": {
 | 
				
			||||||
      "hostname": "localhost",
 | 
					      "hostname": "localhost",
 | 
				
			||||||
 | 
					      "seccomp": 32,
 | 
				
			||||||
 | 
					      "devel": true,
 | 
				
			||||||
      "userns": true,
 | 
					      "userns": true,
 | 
				
			||||||
      "net": true,
 | 
					      "net": true,
 | 
				
			||||||
      "dev": true,
 | 
					      "tty": true,
 | 
				
			||||||
      "syscall": {
 | 
					      "multiarch": true,
 | 
				
			||||||
        "compat": false,
 | 
					 | 
				
			||||||
        "deny_devel": true,
 | 
					 | 
				
			||||||
        "multiarch": true,
 | 
					 | 
				
			||||||
        "linux32": false,
 | 
					 | 
				
			||||||
        "can": false,
 | 
					 | 
				
			||||||
        "bluetooth": false
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "no_new_session": true,
 | 
					 | 
				
			||||||
      "map_real_uid": true,
 | 
					 | 
				
			||||||
      "env": {
 | 
					      "env": {
 | 
				
			||||||
        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "map_real_uid": true,
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
      "filesystem": [
 | 
					      "filesystem": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "src": "/nix/store"
 | 
					          "src": "/nix/store"
 | 
				
			||||||
@ -387,7 +379,7 @@ App
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      "etc": "/etc",
 | 
					      "etc": "/etc",
 | 
				
			||||||
      "auto_etc": true,
 | 
					      "auto_etc": true,
 | 
				
			||||||
      "override": [
 | 
					      "cover": [
 | 
				
			||||||
        "/var/run/nscd"
 | 
					        "/var/run/nscd"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -477,8 +469,8 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		{"valid", state.Entries{testID: testState}, false, false, `    Instance    PID           App    Uptime     Enablements                   Command
 | 
							{"valid", state.Entries{testID: testState}, false, false, `    Instance    PID           App    Uptime     Enablements                  Command
 | 
				
			||||||
    8e2c76b0    3735928559    9      1h2m32s    Wayland, D-Bus, PulseAudio    ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
 | 
					    8e2c76b0    3735928559    9      1h2m32s    wayland, dbus, pulseaudio    ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
		{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
 | 
							{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
 | 
				
			||||||
@ -506,7 +498,8 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
    "pid": 3735928559,
 | 
					    "pid": 3735928559,
 | 
				
			||||||
    "config": {
 | 
					    "config": {
 | 
				
			||||||
      "id": "org.chromium.Chromium",
 | 
					      "id": "org.chromium.Chromium",
 | 
				
			||||||
      "command": [
 | 
					      "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					      "args": [
 | 
				
			||||||
        "chromium",
 | 
					        "chromium",
 | 
				
			||||||
        "--ignore-gpu-blocklist",
 | 
					        "--ignore-gpu-blocklist",
 | 
				
			||||||
        "--disable-smooth-scrolling",
 | 
					        "--disable-smooth-scrolling",
 | 
				
			||||||
@ -523,24 +516,19 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
        "sandbox": {
 | 
					        "sandbox": {
 | 
				
			||||||
          "hostname": "localhost",
 | 
					          "hostname": "localhost",
 | 
				
			||||||
 | 
					          "seccomp": 32,
 | 
				
			||||||
 | 
					          "devel": true,
 | 
				
			||||||
          "userns": true,
 | 
					          "userns": true,
 | 
				
			||||||
          "net": true,
 | 
					          "net": true,
 | 
				
			||||||
          "dev": true,
 | 
					          "tty": true,
 | 
				
			||||||
          "syscall": {
 | 
					          "multiarch": true,
 | 
				
			||||||
            "compat": false,
 | 
					 | 
				
			||||||
            "deny_devel": true,
 | 
					 | 
				
			||||||
            "multiarch": true,
 | 
					 | 
				
			||||||
            "linux32": false,
 | 
					 | 
				
			||||||
            "can": false,
 | 
					 | 
				
			||||||
            "bluetooth": false
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "no_new_session": true,
 | 
					 | 
				
			||||||
          "map_real_uid": true,
 | 
					 | 
				
			||||||
          "env": {
 | 
					          "env": {
 | 
				
			||||||
            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "map_real_uid": true,
 | 
				
			||||||
 | 
					          "dev": true,
 | 
				
			||||||
          "filesystem": [
 | 
					          "filesystem": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "src": "/nix/store"
 | 
					              "src": "/nix/store"
 | 
				
			||||||
@ -573,7 +561,7 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
          "etc": "/etc",
 | 
					          "etc": "/etc",
 | 
				
			||||||
          "auto_etc": true,
 | 
					          "auto_etc": true,
 | 
				
			||||||
          "override": [
 | 
					          "cover": [
 | 
				
			||||||
            "/var/run/nscd"
 | 
					            "/var/run/nscd"
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										252
									
								
								sandbox/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								sandbox/container.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,252 @@
 | 
				
			|||||||
 | 
					// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
 | 
				
			||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HardeningFlags uintptr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						FSyscallCompat HardeningFlags = 1 << iota
 | 
				
			||||||
 | 
						FAllowDevel
 | 
				
			||||||
 | 
						FAllowUserns
 | 
				
			||||||
 | 
						FAllowTTY
 | 
				
			||||||
 | 
						FAllowNet
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
 | 
				
			||||||
 | 
						if flags&FSyscallCompat == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagExt
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowDevel == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyDevel
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowUserns == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyNS
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowTTY == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyTTY
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return opts
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type (
 | 
				
			||||||
 | 
						// Container represents a container environment being prepared or run.
 | 
				
			||||||
 | 
						// None of [Container] methods are safe for concurrent use.
 | 
				
			||||||
 | 
						Container struct {
 | 
				
			||||||
 | 
							// Name of initial process in the container.
 | 
				
			||||||
 | 
							name string
 | 
				
			||||||
 | 
							// Cgroup fd, nil to disable.
 | 
				
			||||||
 | 
							Cgroup *int
 | 
				
			||||||
 | 
							// ExtraFiles passed through to initial process in the container,
 | 
				
			||||||
 | 
							// with behaviour identical to its [exec.Cmd] counterpart.
 | 
				
			||||||
 | 
							ExtraFiles []*os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Custom [exec.Cmd] initialisation function.
 | 
				
			||||||
 | 
							CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// param encoder for shim and init
 | 
				
			||||||
 | 
							setup *gob.Encoder
 | 
				
			||||||
 | 
							// cancels cmd
 | 
				
			||||||
 | 
							cancel context.CancelFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Stdin  io.Reader
 | 
				
			||||||
 | 
							Stdout io.Writer
 | 
				
			||||||
 | 
							Stderr io.Writer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Cancel    func(cmd *exec.Cmd) error
 | 
				
			||||||
 | 
							WaitDelay time.Duration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cmd *exec.Cmd
 | 
				
			||||||
 | 
							ctx context.Context
 | 
				
			||||||
 | 
							Params
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Params holds container configuration and is safe to serialise.
 | 
				
			||||||
 | 
						Params struct {
 | 
				
			||||||
 | 
							// Working directory in the container.
 | 
				
			||||||
 | 
							Dir string
 | 
				
			||||||
 | 
							// Initial process environment.
 | 
				
			||||||
 | 
							Env []string
 | 
				
			||||||
 | 
							// Absolute path of initial process in the container. Overrides name.
 | 
				
			||||||
 | 
							Path string
 | 
				
			||||||
 | 
							// Initial process argv.
 | 
				
			||||||
 | 
							Args []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Mapped Uid in user namespace.
 | 
				
			||||||
 | 
							Uid int
 | 
				
			||||||
 | 
							// Mapped Gid in user namespace.
 | 
				
			||||||
 | 
							Gid int
 | 
				
			||||||
 | 
							// Hostname value in UTS namespace.
 | 
				
			||||||
 | 
							Hostname string
 | 
				
			||||||
 | 
							// Sequential container setup ops.
 | 
				
			||||||
 | 
							*Ops
 | 
				
			||||||
 | 
							// Extra seccomp options.
 | 
				
			||||||
 | 
							Seccomp seccomp.SyscallOpts
 | 
				
			||||||
 | 
							// Permission bits of newly created parent directories.
 | 
				
			||||||
 | 
							// The zero value is interpreted as 0755.
 | 
				
			||||||
 | 
							ParentPerm os.FileMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Flags HardeningFlags
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Ops []Op
 | 
				
			||||||
 | 
						Op  interface {
 | 
				
			||||||
 | 
							early(params *Params) error
 | 
				
			||||||
 | 
							apply(params *Params) error
 | 
				
			||||||
 | 
							prefix() string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Is(op Op) bool
 | 
				
			||||||
 | 
							fmt.Stringer
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Start() error {
 | 
				
			||||||
 | 
						if p.cmd != nil {
 | 
				
			||||||
 | 
							return errors.New("sandbox: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.Ops == nil || len(*p.Ops) == 0 {
 | 
				
			||||||
 | 
							return errors.New("sandbox: starting an empty container")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(p.ctx)
 | 
				
			||||||
 | 
						p.cancel = cancel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var cloneFlags uintptr = syscall.CLONE_NEWIPC |
 | 
				
			||||||
 | 
							syscall.CLONE_NEWUTS |
 | 
				
			||||||
 | 
							syscall.CLONE_NEWCGROUP
 | 
				
			||||||
 | 
						if p.Flags&FAllowNet == 0 {
 | 
				
			||||||
 | 
							cloneFlags |= syscall.CLONE_NEWNET
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map to overflow id to work around ownership checks
 | 
				
			||||||
 | 
						if p.Uid < 1 {
 | 
				
			||||||
 | 
							p.Uid = OverflowUid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.Gid < 1 {
 | 
				
			||||||
 | 
							p.Gid = OverflowGid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.CommandContext != nil {
 | 
				
			||||||
 | 
							p.cmd = p.CommandContext(ctx)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.cmd = exec.CommandContext(ctx, MustExecutable())
 | 
				
			||||||
 | 
							p.cmd.Args = []string{"init"}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
 | 
				
			||||||
 | 
						p.cmd.WaitDelay = p.WaitDelay
 | 
				
			||||||
 | 
						if p.Cancel != nil {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cmd.Dir = "/"
 | 
				
			||||||
 | 
						p.cmd.SysProcAttr = &syscall.SysProcAttr{
 | 
				
			||||||
 | 
							Setsid:    p.Flags&FAllowTTY == 0,
 | 
				
			||||||
 | 
							Pdeathsig: syscall.SIGKILL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Cloneflags: cloneFlags |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWUSER |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWPID |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWNS,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// remain privileged for setup
 | 
				
			||||||
 | 
							AmbientCaps: []uintptr{CAP_SYS_ADMIN},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							UseCgroupFD: p.Cgroup != nil,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.cmd.SysProcAttr.UseCgroupFD {
 | 
				
			||||||
 | 
							p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// place setup pipe before user supplied extra files, this is later restored by init
 | 
				
			||||||
 | 
						if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
 | 
								"cannot create shim setup pipe:")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.setup = e
 | 
				
			||||||
 | 
							p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg.Verbose("starting container init")
 | 
				
			||||||
 | 
						if err := p.cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Serve() error {
 | 
				
			||||||
 | 
						if p.setup == nil {
 | 
				
			||||||
 | 
							panic("invalid serve")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup := p.setup
 | 
				
			||||||
 | 
						p.setup = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.Path != "" && !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EINVAL,
 | 
				
			||||||
 | 
								fmt.Sprintf("invalid executable path %q", p.Path))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.Path == "" {
 | 
				
			||||||
 | 
							if p.name == "" {
 | 
				
			||||||
 | 
								p.Path = os.Getenv("SHELL")
 | 
				
			||||||
 | 
								if !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
									p.cancel()
 | 
				
			||||||
 | 
									return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
										"no command specified and $SHELL is invalid")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.name = path.Base(p.Path)
 | 
				
			||||||
 | 
							} else if path.IsAbs(p.name) {
 | 
				
			||||||
 | 
								p.Path = p.name
 | 
				
			||||||
 | 
							} else if v, err := exec.LookPath(p.name); err != nil {
 | 
				
			||||||
 | 
								p.cancel()
 | 
				
			||||||
 | 
								return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								p.Path = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := setup.Encode(
 | 
				
			||||||
 | 
							&initParams{
 | 
				
			||||||
 | 
								p.Params,
 | 
				
			||||||
 | 
								syscall.Getuid(),
 | 
				
			||||||
 | 
								syscall.Getgid(),
 | 
				
			||||||
 | 
								len(p.ExtraFiles),
 | 
				
			||||||
 | 
								msg.IsVerbose(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) String() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
 | 
				
			||||||
 | 
							p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(ctx context.Context, name string, args ...string) *Container {
 | 
				
			||||||
 | 
						return &Container{name: name, ctx: ctx,
 | 
				
			||||||
 | 
							Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										255
									
								
								sandbox/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								sandbox/container_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,255 @@
 | 
				
			|||||||
 | 
					package sandbox_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ignore  = "\x00"
 | 
				
			||||||
 | 
						ignoreV = -1
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainer(t *testing.T) {
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							oldVerbose := fmsg.Load()
 | 
				
			||||||
 | 
							oldOutput := sandbox.GetOutput()
 | 
				
			||||||
 | 
							internal.InstallFmsg(true)
 | 
				
			||||||
 | 
							t.Cleanup(func() { fmsg.Store(oldVerbose) })
 | 
				
			||||||
 | 
							t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name  string
 | 
				
			||||||
 | 
							flags sandbox.HardeningFlags
 | 
				
			||||||
 | 
							ops   *sandbox.Ops
 | 
				
			||||||
 | 
							mnt   []*vfs.MountInfoEntry
 | 
				
			||||||
 | 
							host  string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
 | 
				
			||||||
 | 
							{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
 | 
				
			||||||
 | 
								new(sandbox.Ops), nil, "test-minimal"},
 | 
				
			||||||
 | 
							{"tmpfs", 0,
 | 
				
			||||||
 | 
								new(sandbox.Ops).
 | 
				
			||||||
 | 
									Tmpfs(fst.Tmp, 0, 0755),
 | 
				
			||||||
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
 | 
									e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
 | 
								}, "test-tmpfs"},
 | 
				
			||||||
 | 
							{"dev", sandbox.FAllowTTY, // go test output is not a tty
 | 
				
			||||||
 | 
								new(sandbox.Ops).
 | 
				
			||||||
 | 
									Dev("/dev").
 | 
				
			||||||
 | 
									Mqueue("/dev/mqueue"),
 | 
				
			||||||
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
 | 
									e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
 | 
				
			||||||
 | 
									e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
 | 
				
			||||||
 | 
								}, ""},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
								defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
 | 
				
			||||||
 | 
									"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
 | 
				
			||||||
 | 
								container.Uid = 1000
 | 
				
			||||||
 | 
								container.Gid = 100
 | 
				
			||||||
 | 
								container.Hostname = tc.host
 | 
				
			||||||
 | 
								container.CommandContext = commandContext
 | 
				
			||||||
 | 
								container.Flags |= tc.flags
 | 
				
			||||||
 | 
								container.Stdout, container.Stderr = os.Stdout, os.Stderr
 | 
				
			||||||
 | 
								container.Ops = tc.ops
 | 
				
			||||||
 | 
								if container.Args[5] == "" {
 | 
				
			||||||
 | 
									if name, err := os.Hostname(); err != nil {
 | 
				
			||||||
 | 
										t.Fatalf("cannot get hostname: %v", err)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										container.Args[5] = name
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								container.
 | 
				
			||||||
 | 
									Tmpfs("/tmp", 0, 0755).
 | 
				
			||||||
 | 
									Bind(os.Args[0], os.Args[0], 0).
 | 
				
			||||||
 | 
									Mkdir("/usr/bin", 0755).
 | 
				
			||||||
 | 
									Link(os.Args[0], "/usr/bin/sandbox.test").
 | 
				
			||||||
 | 
									Place("/etc/hostname", []byte(container.Args[5]))
 | 
				
			||||||
 | 
								// in case test has cgo enabled
 | 
				
			||||||
 | 
								var libPaths []string
 | 
				
			||||||
 | 
								if entries, err := ldd.ExecFilter(ctx,
 | 
				
			||||||
 | 
									commandContext,
 | 
				
			||||||
 | 
									func(v []byte) []byte {
 | 
				
			||||||
 | 
										return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
 | 
				
			||||||
 | 
									}, os.Args[0]); err != nil {
 | 
				
			||||||
 | 
									log.Fatalf("ldd: %v", err)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									libPaths = ldd.Path(entries)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, name := range libPaths {
 | 
				
			||||||
 | 
									container.Bind(name, name, 0)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// needs /proc to check mountinfo
 | 
				
			||||||
 | 
								container.Proc("/proc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
 | 
				
			||||||
 | 
								mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
 | 
				
			||||||
 | 
								mnt = append(mnt, tc.mnt...)
 | 
				
			||||||
 | 
								mnt = append(mnt,
 | 
				
			||||||
 | 
									e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
 | 
									e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
 | 
				
			||||||
 | 
									e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								for _, name := range libPaths {
 | 
				
			||||||
 | 
									mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
 | 
				
			||||||
 | 
								want := new(bytes.Buffer)
 | 
				
			||||||
 | 
								if err := gob.NewEncoder(want).Encode(mnt); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("cannot serialise expected mount points: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								container.Stdin = want
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := container.Start(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "start:")
 | 
				
			||||||
 | 
									t.Fatalf("cannot start container: %v", err)
 | 
				
			||||||
 | 
								} else if err = container.Serve(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "serve:")
 | 
				
			||||||
 | 
									t.Errorf("cannot serve setup params: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := container.Wait(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "wait:")
 | 
				
			||||||
 | 
									t.Fatalf("wait: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
 | 
				
			||||||
 | 
						return &vfs.MountInfoEntry{
 | 
				
			||||||
 | 
							ID:        ignoreV,
 | 
				
			||||||
 | 
							Parent:    ignoreV,
 | 
				
			||||||
 | 
							Devno:     vfs.DevT{ignoreV, ignoreV},
 | 
				
			||||||
 | 
							Root:      root,
 | 
				
			||||||
 | 
							Target:    target,
 | 
				
			||||||
 | 
							VfsOptstr: vfsOptstr,
 | 
				
			||||||
 | 
							OptFields: []string{ignore},
 | 
				
			||||||
 | 
							FsType:    fsType,
 | 
				
			||||||
 | 
							Source:    source,
 | 
				
			||||||
 | 
							FsOptstr:  fsOptstr,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainerString(t *testing.T) {
 | 
				
			||||||
 | 
						container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
 | 
				
			||||||
 | 
						container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
 | 
						container.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
						want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
 | 
				
			||||||
 | 
						if got := container.String(); got != want {
 | 
				
			||||||
 | 
							t.Errorf("String: %s, want %s", got, want)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperCheckContainer(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 6 || os.Args[4] != "check" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("user", func(t *testing.T) {
 | 
				
			||||||
 | 
							if uid := syscall.Getuid(); uid != 1000 {
 | 
				
			||||||
 | 
								t.Errorf("Getuid: %d, want 1000", uid)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if gid := syscall.Getgid(); gid != 100 {
 | 
				
			||||||
 | 
								t.Errorf("Getgid: %d, want 100", gid)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("hostname", func(t *testing.T) {
 | 
				
			||||||
 | 
							if name, err := os.Hostname(); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot get hostname: %v", err)
 | 
				
			||||||
 | 
							} else if name != os.Args[5] {
 | 
				
			||||||
 | 
								t.Errorf("Hostname: %q, want %q", name, os.Args[5])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if p, err := os.ReadFile("/etc/hostname"); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("%v", err)
 | 
				
			||||||
 | 
							} else if string(p) != os.Args[5] {
 | 
				
			||||||
 | 
								t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("mount", func(t *testing.T) {
 | 
				
			||||||
 | 
							var mnt []*vfs.MountInfoEntry
 | 
				
			||||||
 | 
							if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot receive expected mount points: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var d *vfs.MountInfoDecoder
 | 
				
			||||||
 | 
							if f, err := os.Open("/proc/self/mountinfo"); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot open mountinfo: %v", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								d = vfs.NewMountInfoDecoder(f)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							i := 0
 | 
				
			||||||
 | 
							for cur := range d.Entries() {
 | 
				
			||||||
 | 
								if i == len(mnt) {
 | 
				
			||||||
 | 
									t.Errorf("got more than %d entries", len(mnt))
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
 | 
				
			||||||
 | 
								cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
 | 
				
			||||||
 | 
								cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
 | 
				
			||||||
 | 
								mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
 | 
				
			||||||
 | 
								mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !cur.EqualWithIgnore(mnt[i], "\x00") {
 | 
				
			||||||
 | 
									t.Errorf("[FAIL] %s", cur)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									t.Logf("[ OK ] %s", cur)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								i++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := d.Err(); err != nil {
 | 
				
			||||||
 | 
								t.Errorf("cannot parse mountinfo: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if i != len(mnt) {
 | 
				
			||||||
 | 
								t.Errorf("got %d entries, want %d", i, len(mnt))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func commandContext(ctx context.Context) *exec.Cmd {
 | 
				
			||||||
 | 
						return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
							"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
package internal
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
@ -15,7 +13,7 @@ var (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func copyExecutable() {
 | 
					func copyExecutable() {
 | 
				
			||||||
	if name, err := os.Executable(); err != nil {
 | 
						if name, err := os.Executable(); err != nil {
 | 
				
			||||||
		fmsg.BeforeExit()
 | 
							msg.BeforeExit()
 | 
				
			||||||
		log.Fatalf("cannot read executable path: %v", err)
 | 
							log.Fatalf("cannot read executable path: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		executable = name
 | 
							executable = name
 | 
				
			||||||
							
								
								
									
										17
									
								
								sandbox/executable_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sandbox/executable_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package sandbox_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestExecutable(t *testing.T) {
 | 
				
			||||||
 | 
						for i := 0; i < 16; i++ {
 | 
				
			||||||
 | 
							if got := sandbox.MustExecutable(); got != os.Args[0] {
 | 
				
			||||||
 | 
								t.Errorf("MustExecutable: %q, want %q",
 | 
				
			||||||
 | 
									got, os.Args[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										362
									
								
								sandbox/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								sandbox/init.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,362 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// time to wait for linger processes after death of initial process
 | 
				
			||||||
 | 
						residualProcessTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// intermediate tmpfs mount point
 | 
				
			||||||
 | 
						basePath = "/tmp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// setup params file descriptor
 | 
				
			||||||
 | 
						setupEnv = "FORTIFY_SETUP"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type initParams struct {
 | 
				
			||||||
 | 
						Params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						HostUid, HostGid int
 | 
				
			||||||
 | 
						// extra files count
 | 
				
			||||||
 | 
						Count int
 | 
				
			||||||
 | 
						// verbosity pass through
 | 
				
			||||||
 | 
						Verbose bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			||||||
 | 
						runtime.LockOSThread()
 | 
				
			||||||
 | 
						prepare("init")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if os.Getpid() != 1 {
 | 
				
			||||||
 | 
							log.Fatal("this process must run as pid 1")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							receive setup payload
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							params      initParams
 | 
				
			||||||
 | 
							closeSetup  func() error
 | 
				
			||||||
 | 
							setupFile   *os.File
 | 
				
			||||||
 | 
							offsetSetup int
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, ErrInvalid) {
 | 
				
			||||||
 | 
								log.Fatal("invalid setup descriptor")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, ErrNotSet) {
 | 
				
			||||||
 | 
								log.Fatal("FORTIFY_SETUP not set")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Fatalf("cannot decode init setup payload: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if params.Ops == nil {
 | 
				
			||||||
 | 
								log.Fatal("invalid setup parameters")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if params.ParentPerm == 0 {
 | 
				
			||||||
 | 
								params.ParentPerm = 0755
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							setVerbose(params.Verbose)
 | 
				
			||||||
 | 
							msg.Verbose("received setup parameters")
 | 
				
			||||||
 | 
							closeSetup = f
 | 
				
			||||||
 | 
							offsetSetup = int(setupFile.Fd() + 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// write uid/gid map here so parent does not need to set dumpable
 | 
				
			||||||
 | 
						if err := SetDumpable(SUID_DUMP_USER); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/uid_map",
 | 
				
			||||||
 | 
							append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
 | 
				
			||||||
 | 
							0); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/setgroups",
 | 
				
			||||||
 | 
							[]byte("deny\n"),
 | 
				
			||||||
 | 
							0); err != nil && !os.IsNotExist(err) {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/gid_map",
 | 
				
			||||||
 | 
							append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
 | 
				
			||||||
 | 
							0); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oldmask := syscall.Umask(0)
 | 
				
			||||||
 | 
						if params.Hostname != "" {
 | 
				
			||||||
 | 
							if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot set hostname: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							set up mount points from intermediate root
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount("", "/", "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot make / rslave: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, op := range *params.Ops {
 | 
				
			||||||
 | 
							if op == nil {
 | 
				
			||||||
 | 
								log.Fatalf("invalid op %d", i)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := op.early(¶ms.Params); err != nil {
 | 
				
			||||||
 | 
								msg.PrintBaseErr(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot prepare op %d:", i))
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount("rootfs", basePath, "tmpfs",
 | 
				
			||||||
 | 
							syscall.MS_NODEV|syscall.MS_NOSUID,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot mount intermediate root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.Chdir(basePath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot enter base path: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(sysrootDir, 0755); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.Mount(sysrootDir, sysrootDir, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot bind sysroot: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(hostDir, 0755); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.PivotRoot(basePath, hostDir); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot pivot into intermediate root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.Chdir("/"); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, op := range *params.Ops {
 | 
				
			||||||
 | 
							// ops already checked during early setup
 | 
				
			||||||
 | 
							msg.Verbosef("%s %s", op.prefix(), op)
 | 
				
			||||||
 | 
							if err := op.apply(¶ms.Params); err != nil {
 | 
				
			||||||
 | 
								msg.PrintBaseErr(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot apply op %d:", i))
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							pivot to sysroot
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount(hostDir, hostDir, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot make host root rprivate: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot unmount host root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							var fd int
 | 
				
			||||||
 | 
							if err := IgnoringEINTR(func() (err error) {
 | 
				
			||||||
 | 
								fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot open intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := os.Chdir(sysrootPath); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("%v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := syscall.PivotRoot(".", "."); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot pivot into sysroot: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := syscall.Fchdir(fd); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot re-enter intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot unmount intemediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := os.Chdir("/"); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("%v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := syscall.Close(fd); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot close intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							load seccomp filter
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, _, err := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); err != 0 {
 | 
				
			||||||
 | 
							log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot load syscall filter: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* at this point CAP_SYS_ADMIN can be dropped, however it is kept for now as it does not increase attack surface */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							pass through extra files
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						extraFiles := make([]*os.File, params.Count)
 | 
				
			||||||
 | 
						for i := range extraFiles {
 | 
				
			||||||
 | 
							extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						syscall.Umask(oldmask)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							prepare initial process
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd := exec.Command(params.Path)
 | 
				
			||||||
 | 
						cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						cmd.Args = params.Args
 | 
				
			||||||
 | 
						cmd.Env = params.Env
 | 
				
			||||||
 | 
						cmd.ExtraFiles = extraFiles
 | 
				
			||||||
 | 
						cmd.Dir = params.Dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						msg.Suspend()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							close setup pipe
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := closeSetup(); err != nil {
 | 
				
			||||||
 | 
							log.Println("cannot close setup pipe:", err)
 | 
				
			||||||
 | 
							// not fatal
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							perform init duties
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sig := make(chan os.Signal, 2)
 | 
				
			||||||
 | 
						signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type winfo struct {
 | 
				
			||||||
 | 
							wpid    int
 | 
				
			||||||
 | 
							wstatus syscall.WaitStatus
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						info := make(chan winfo, 1)
 | 
				
			||||||
 | 
						done := make(chan struct{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								err     error
 | 
				
			||||||
 | 
								wpid    = -2
 | 
				
			||||||
 | 
								wstatus syscall.WaitStatus
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// keep going until no child process is left
 | 
				
			||||||
 | 
							for wpid != -1 {
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if wpid != -2 {
 | 
				
			||||||
 | 
									info <- winfo{wpid, wstatus}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = syscall.EINTR
 | 
				
			||||||
 | 
								for errors.Is(err, syscall.EINTR) {
 | 
				
			||||||
 | 
									wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !errors.Is(err, syscall.ECHILD) {
 | 
				
			||||||
 | 
								log.Println("unexpected wait4 response:", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							close(done)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// closed after residualProcessTimeout has elapsed after initial process death
 | 
				
			||||||
 | 
						timeout := make(chan struct{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						r := 2
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case s := <-sig:
 | 
				
			||||||
 | 
								if msg.Resume() {
 | 
				
			||||||
 | 
									msg.Verbosef("terminating on %s after process start", s.String())
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									msg.Verbosef("terminating on %s", s.String())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(0)
 | 
				
			||||||
 | 
							case w := <-info:
 | 
				
			||||||
 | 
								if w.wpid == cmd.Process.Pid {
 | 
				
			||||||
 | 
									// initial process exited, output is most likely available again
 | 
				
			||||||
 | 
									msg.Resume()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									switch {
 | 
				
			||||||
 | 
									case w.wstatus.Exited():
 | 
				
			||||||
 | 
										r = w.wstatus.ExitStatus()
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
 | 
				
			||||||
 | 
									case w.wstatus.Signaled():
 | 
				
			||||||
 | 
										r = 128 + int(w.wstatus.Signal())
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
 | 
				
			||||||
 | 
									default:
 | 
				
			||||||
 | 
										r = 255
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with status %#x", w.wstatus)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									go func() {
 | 
				
			||||||
 | 
										time.Sleep(residualProcessTimeout)
 | 
				
			||||||
 | 
										close(timeout)
 | 
				
			||||||
 | 
									}()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case <-done:
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(r)
 | 
				
			||||||
 | 
							case <-timeout:
 | 
				
			||||||
 | 
								log.Println("timeout exceeded waiting for lingering processes")
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(r)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TryArgv0 calls [Init] if the last element of argv0 is "init".
 | 
				
			||||||
 | 
					func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			||||||
 | 
						if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
 | 
				
			||||||
 | 
							msg = v
 | 
				
			||||||
 | 
							Init(prepare, setVerbose)
 | 
				
			||||||
 | 
							msg.BeforeExit()
 | 
				
			||||||
 | 
							os.Exit(0)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										125
									
								
								sandbox/mount.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								sandbox/mount.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,125 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
 | 
				
			||||||
 | 
						if eq {
 | 
				
			||||||
 | 
							msg.Verbosef("resolved %q flags %#x", target, flags)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount(source, target, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot mount %q on %q:", source, target))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var targetFinal string
 | 
				
			||||||
 | 
						if v, err := filepath.EvalSymlinks(target); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							targetFinal = v
 | 
				
			||||||
 | 
							if targetFinal != target {
 | 
				
			||||||
 | 
								msg.Verbosef("target resolves to %q", targetFinal)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// final target path according to the kernel through proc
 | 
				
			||||||
 | 
						var targetKFinal string
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							var destFd int
 | 
				
			||||||
 | 
							if err := IgnoringEINTR(func() (err error) {
 | 
				
			||||||
 | 
								destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot open %q:", targetFinal))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if v, err := os.Readlink(p.fd(destFd)); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							} else if err = syscall.Close(destFd); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot close %q:", targetFinal))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								targetKFinal = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
 | 
				
			||||||
 | 
						return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
 | 
				
			||||||
 | 
							n, err := d.Unfold(targetKFinal)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, syscall.ESTALE) {
 | 
				
			||||||
 | 
									return msg.WrapErr(err,
 | 
				
			||||||
 | 
										fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot unfold mount hierarchy:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = remountWithFlags(n, mf); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if flags&syscall.MS_REC == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for cur := range n.Collective() {
 | 
				
			||||||
 | 
								err = remountWithFlags(cur, mf)
 | 
				
			||||||
 | 
								if err != nil && !errors.Is(err, syscall.EACCES) {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
 | 
				
			||||||
 | 
						kf, unmatched := n.Flags()
 | 
				
			||||||
 | 
						if len(unmatched) != 0 {
 | 
				
			||||||
 | 
							msg.Verbosef("unmatched vfs options: %q", unmatched)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if kf&mf != mf {
 | 
				
			||||||
 | 
							return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
 | 
				
			||||||
 | 
								syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
 | 
				
			||||||
 | 
								""),
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot remount %q:", n.Clean))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
				
			||||||
 | 
						target := toSysroot(name)
 | 
				
			||||||
 | 
						if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						opt := fmt.Sprintf("mode=%#o", perm)
 | 
				
			||||||
 | 
						if size > 0 {
 | 
				
			||||||
 | 
							opt += fmt.Sprintf(",size=%d", size)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
 | 
				
			||||||
 | 
							syscall.MS_NOSUID|syscall.MS_NODEV, opt),
 | 
				
			||||||
 | 
							fmt.Sprintf("cannot mount tmpfs on %q:", name))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parentPerm(perm os.FileMode) os.FileMode {
 | 
				
			||||||
 | 
						pperm := 0755
 | 
				
			||||||
 | 
						if perm&0070 == 0 {
 | 
				
			||||||
 | 
							pperm &= ^0050
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if perm&0007 == 0 {
 | 
				
			||||||
 | 
							pperm &= ^0005
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return os.FileMode(pperm)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								sandbox/msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								sandbox/msg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Msg interface {
 | 
				
			||||||
 | 
						IsVerbose() bool
 | 
				
			||||||
 | 
						Verbose(v ...any)
 | 
				
			||||||
 | 
						Verbosef(format string, v ...any)
 | 
				
			||||||
 | 
						WrapErr(err error, a ...any) error
 | 
				
			||||||
 | 
						PrintBaseErr(err error, fallback string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Suspend()
 | 
				
			||||||
 | 
						Resume() bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						BeforeExit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DefaultMsg struct{ inactive atomic.Bool }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) IsVerbose() bool { return true }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Verbose(v ...any) {
 | 
				
			||||||
 | 
						if !msg.inactive.Load() {
 | 
				
			||||||
 | 
							log.Println(v...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Verbosef(format string, v ...any) {
 | 
				
			||||||
 | 
						if !msg.inactive.Load() {
 | 
				
			||||||
 | 
							log.Printf(format, v...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
 | 
				
			||||||
 | 
						log.Println(a...)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Suspend()     { msg.inactive.Store(true) }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) BeforeExit()  {}
 | 
				
			||||||
							
								
								
									
										26
									
								
								sandbox/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								sandbox/output.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var msg Msg = new(DefaultMsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetOutput() Msg { return msg }
 | 
				
			||||||
 | 
					func SetOutput(v Msg) {
 | 
				
			||||||
 | 
						if v == nil {
 | 
				
			||||||
 | 
							msg = new(DefaultMsg)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func wrapErrSuffix(err error, a ...any) error {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return msg.WrapErr(err, append(a, err)...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func wrapErrSelf(err error) error {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								sandbox/overflow.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								sandbox/overflow.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ofUid  int
 | 
				
			||||||
 | 
						ofGid  int
 | 
				
			||||||
 | 
						ofOnce sync.Once
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ofUidPath = "/proc/sys/kernel/overflowuid"
 | 
				
			||||||
 | 
						ofGidPath = "/proc/sys/kernel/overflowgid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mustReadOverflow() {
 | 
				
			||||||
 | 
						if v, err := os.ReadFile(ofUidPath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot read %q: %v", ofUidPath, err)
 | 
				
			||||||
 | 
						} else if ofUid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot interpret %q: %v", ofUidPath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if v, err := os.ReadFile(ofGidPath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot read %q: %v", ofGidPath, err)
 | 
				
			||||||
 | 
						} else if ofGid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot interpret %q: %v", ofGidPath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func OverflowUid() int { ofOnce.Do(mustReadOverflow); return ofUid }
 | 
				
			||||||
 | 
					func OverflowGid() int { ofOnce.Do(mustReadOverflow); return ofGid }
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package proc
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
@ -12,7 +12,7 @@ var (
 | 
				
			|||||||
	ErrInvalid = errors.New("bad file descriptor")
 | 
						ErrInvalid = errors.New("bad file descriptor")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Setup appends the read end of a pipe for payload transmission and returns its fd.
 | 
					// Setup appends the read end of a pipe for setup params transmission and returns its fd.
 | 
				
			||||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
					func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
				
			||||||
	if r, w, err := os.Pipe(); err != nil {
 | 
						if r, w, err := os.Pipe(); err != nil {
 | 
				
			||||||
		return -1, nil, err
 | 
							return -1, nil, err
 | 
				
			||||||
@ -23,9 +23,8 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Receive retrieves payload pipe fd from the environment,
 | 
					// Receive retrieves setup fd from the environment and receives params.
 | 
				
			||||||
// receives its payload and returns the Close method of the pipe.
 | 
					func Receive(key string, e any, v **os.File) (func() error, error) {
 | 
				
			||||||
func Receive(key string, e any) (func() error, error) {
 | 
					 | 
				
			||||||
	var setup *os.File
 | 
						var setup *os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s, ok := os.LookupEnv(key); !ok {
 | 
						if s, ok := os.LookupEnv(key); !ok {
 | 
				
			||||||
@ -38,8 +37,11 @@ func Receive(key string, e any) (func() error, error) {
 | 
				
			|||||||
			if setup == nil {
 | 
								if setup == nil {
 | 
				
			||||||
				return nil, ErrInvalid
 | 
									return nil, ErrInvalid
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if v != nil {
 | 
				
			||||||
 | 
									*v = setup
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
 | 
						return setup.Close, gob.NewDecoder(setup).Decode(e)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										94
									
								
								sandbox/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								sandbox/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						hostPath    = "/" + hostDir
 | 
				
			||||||
 | 
						hostDir     = "host"
 | 
				
			||||||
 | 
						sysrootPath = "/" + sysrootDir
 | 
				
			||||||
 | 
						sysrootDir  = "sysroot"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toSysroot(name string) string {
 | 
				
			||||||
 | 
						name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
 | 
				
			||||||
 | 
						return path.Join(sysrootPath, name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toHost(name string) string {
 | 
				
			||||||
 | 
						name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
 | 
				
			||||||
 | 
						return path.Join(hostPath, name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createFile(name string, perm, pperm os.FileMode, content []byte) error {
 | 
				
			||||||
 | 
						if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if content != nil {
 | 
				
			||||||
 | 
							_, err = f.Write(content)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return errors.Join(f.Close(), err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ensureFile(name string, perm, pperm os.FileMode) error {
 | 
				
			||||||
 | 
						fi, err := os.Stat(name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if !os.IsNotExist(err) {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return createFile(name, perm, pperm, nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
 | 
				
			||||||
 | 
							err = msg.WrapErr(syscall.EISDIR,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is a directory", name))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var hostProc = newProcPats(hostPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newProcPats(prefix string) *procPaths {
 | 
				
			||||||
 | 
						return &procPaths{prefix + "/proc", prefix + "/proc/self"}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type procPaths struct {
 | 
				
			||||||
 | 
						prefix string
 | 
				
			||||||
 | 
						self   string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *procPaths) stdout() string   { return p.self + "/fd/1" }
 | 
				
			||||||
 | 
					func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
 | 
				
			||||||
 | 
					func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
 | 
				
			||||||
 | 
						if r, err := os.Open(p.self + "/mountinfo"); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							d := vfs.NewMountInfoDecoder(r)
 | 
				
			||||||
 | 
							err0 := f(d)
 | 
				
			||||||
 | 
							if err = r.Close(); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot close mountinfo:")
 | 
				
			||||||
 | 
							} else if err = d.Err(); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot parse mountinfo:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return err0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -11,6 +11,9 @@ import (
 | 
				
			|||||||
// New returns an inactive Encoder instance.
 | 
					// New returns an inactive Encoder instance.
 | 
				
			||||||
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
 | 
					func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load loads a filter into the kernel.
 | 
				
			||||||
 | 
					func Load(opts SyscallOpts) error { return buildFilter(-1, opts) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
An Encoder writes a BPF program to an output stream.
 | 
					An Encoder writes a BPF program to an output stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,7 +28,7 @@ func (e *exporter) prepare() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		ec := make(chan error, 1)
 | 
							ec := make(chan error, 1)
 | 
				
			||||||
		go func(fd uintptr) {
 | 
							go func(fd uintptr) {
 | 
				
			||||||
			ec <- exportFilter(fd, e.opts)
 | 
								ec <- buildFilter(int(fd), e.opts)
 | 
				
			||||||
			close(ec)
 | 
								close(ec)
 | 
				
			||||||
			_ = e.closeWrite()
 | 
								_ = e.closeWrite()
 | 
				
			||||||
			runtime.KeepAlive(e.w)
 | 
								runtime.KeepAlive(e.w)
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user