Compare commits
	
		
			34 Commits
		
	
	
		
			2e3bb1893e
			...
			be4d8b6300
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| be4d8b6300 | |||
| 3e11ce6868 | |||
| 562f5ed797 | |||
| db03565614 | |||
| 7d99e45b88 | |||
| 1651eb06df | |||
| ac543a1ce8 | |||
| e2489059c1 | |||
| 2e3f6a4c51 | |||
| 2162029f46 | |||
| a1148edd00 | |||
| 6acd0d4e88 | |||
| 35b7142317 | |||
| c4d6651cae | |||
| 22a4b99674 | |||
| 1464ef774b | |||
| 66ba4cea5c | |||
| f8d0786509 | |||
| 56a73bb019 | |||
| fb8abf63db | |||
| 63802c5f0d | |||
| aff80b6b00 | |||
| a98a176907 | |||
| 5302879b88 | |||
| 891b3cbde7 | |||
| c795293f36 | |||
| 42e1043300 | |||
| 5416b07daa | |||
| e57a0e9bf2 | |||
| ab48706ebe | |||
| c1a459a0b1 | |||
| 5125e96ecf | |||
| e0e2f40e84 | |||
| bf8094c6ca | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,7 @@
 | 
			
		||||
*.dll
 | 
			
		||||
*.so
 | 
			
		||||
*.dylib
 | 
			
		||||
*.pkg
 | 
			
		||||
/fortify
 | 
			
		||||
 | 
			
		||||
# Test binary, built with `go test -c`
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										202
									
								
								bundle.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								bundle.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
			
		||||
{
 | 
			
		||||
  nixpkgsFor,
 | 
			
		||||
  system,
 | 
			
		||||
  nixpkgs,
 | 
			
		||||
  home-manager,
 | 
			
		||||
}:
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  writeScript,
 | 
			
		||||
  runtimeShell,
 | 
			
		||||
  writeText,
 | 
			
		||||
  symlinkJoin,
 | 
			
		||||
  vmTools,
 | 
			
		||||
  runCommand,
 | 
			
		||||
  fetchFromGitHub,
 | 
			
		||||
 | 
			
		||||
  nix,
 | 
			
		||||
 | 
			
		||||
  name ? throw "name is required",
 | 
			
		||||
  version ? throw "version is required",
 | 
			
		||||
  pname ? "${name}-${version}",
 | 
			
		||||
  modules ? [ ],
 | 
			
		||||
  script ? ''
 | 
			
		||||
    exec "$SHELL" "$@"
 | 
			
		||||
  '',
 | 
			
		||||
 | 
			
		||||
  id ? name,
 | 
			
		||||
  app_id ? throw "app_id is required",
 | 
			
		||||
  groups ? [ ],
 | 
			
		||||
  userns ? false,
 | 
			
		||||
  net ? true,
 | 
			
		||||
  dev ? false,
 | 
			
		||||
  no_new_session ? false,
 | 
			
		||||
  map_real_uid ? false,
 | 
			
		||||
  direct_wayland ? false,
 | 
			
		||||
  system_bus ? null,
 | 
			
		||||
  session_bus ? null,
 | 
			
		||||
 | 
			
		||||
  allow_wayland ? true,
 | 
			
		||||
  allow_x11 ? false,
 | 
			
		||||
  allow_dbus ? true,
 | 
			
		||||
  allow_pulse ? true,
 | 
			
		||||
  gpu ? allow_wayland || allow_x11,
 | 
			
		||||
}:
 | 
			
		||||
 | 
			
		||||
let
 | 
			
		||||
  inherit (lib) optionals;
 | 
			
		||||
 | 
			
		||||
  homeManagerConfiguration = home-manager.lib.homeManagerConfiguration {
 | 
			
		||||
    pkgs = nixpkgsFor.${system};
 | 
			
		||||
    modules = modules ++ [
 | 
			
		||||
      {
 | 
			
		||||
        home = {
 | 
			
		||||
          username = "fortify";
 | 
			
		||||
          homeDirectory = "/data/data/${id}";
 | 
			
		||||
          stateVersion = "22.11";
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    ];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  launcher = writeScript "fortify-${pname}" ''
 | 
			
		||||
    #!${runtimeShell} -el
 | 
			
		||||
    ${script}
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  extraNixOSConfig =
 | 
			
		||||
    { pkgs, ... }:
 | 
			
		||||
    {
 | 
			
		||||
      environment = {
 | 
			
		||||
        etc.nixpkgs.source = nixpkgs.outPath;
 | 
			
		||||
        systemPackages = [ pkgs.nix ];
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  nixos = nixpkgs.lib.nixosSystem {
 | 
			
		||||
    inherit system;
 | 
			
		||||
    modules = [
 | 
			
		||||
      extraNixOSConfig
 | 
			
		||||
      { nix.settings.experimental-features = [ "flakes" ]; }
 | 
			
		||||
      { nix.settings.experimental-features = [ "nix-command" ]; }
 | 
			
		||||
      { boot.isContainer = true; }
 | 
			
		||||
      { system.stateVersion = "22.11"; }
 | 
			
		||||
    ];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  etc = vmTools.runInLinuxVM (
 | 
			
		||||
    runCommand "etc" { } ''
 | 
			
		||||
      mkdir -p /etc
 | 
			
		||||
      ${nixos.config.system.build.etcActivationCommands}
 | 
			
		||||
 | 
			
		||||
      # remove unused files
 | 
			
		||||
      rm -rf /etc/sudoers
 | 
			
		||||
 | 
			
		||||
      mkdir -p $out
 | 
			
		||||
      tar -C /etc -cf "$out/etc.tar" .
 | 
			
		||||
    ''
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  extendSessionDefault = id: ext: {
 | 
			
		||||
    filter = true;
 | 
			
		||||
 | 
			
		||||
    talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
 | 
			
		||||
    own =
 | 
			
		||||
      (optionals (id != null) [
 | 
			
		||||
        "${id}.*"
 | 
			
		||||
        "org.mpris.MediaPlayer2.${id}.*"
 | 
			
		||||
      ])
 | 
			
		||||
      ++ ext.own;
 | 
			
		||||
 | 
			
		||||
    inherit (ext) call broadcast;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  nixGL = fetchFromGitHub {
 | 
			
		||||
    owner = "nix-community";
 | 
			
		||||
    repo = "nixGL";
 | 
			
		||||
    rev = "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
 | 
			
		||||
    hash = "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  mesaWrappers =
 | 
			
		||||
    let
 | 
			
		||||
      isIntelX86Platform = system == "x86_64-linux";
 | 
			
		||||
      nixGLPackages = import (nixGL + "/default.nix") {
 | 
			
		||||
        pkgs = nixpkgs.legacyPackages.${system};
 | 
			
		||||
        enable32bits = isIntelX86Platform;
 | 
			
		||||
        enableIntelX86Extensions = isIntelX86Platform;
 | 
			
		||||
      };
 | 
			
		||||
    in
 | 
			
		||||
    symlinkJoin {
 | 
			
		||||
      name = "nixGL-mesa";
 | 
			
		||||
      paths = with nixGLPackages; [
 | 
			
		||||
        nixGLIntel
 | 
			
		||||
        nixVulkanIntel
 | 
			
		||||
      ];
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  info = builtins.toJSON {
 | 
			
		||||
    inherit
 | 
			
		||||
      name
 | 
			
		||||
      version
 | 
			
		||||
      id
 | 
			
		||||
      app_id
 | 
			
		||||
      launcher
 | 
			
		||||
      groups
 | 
			
		||||
      userns
 | 
			
		||||
      net
 | 
			
		||||
      dev
 | 
			
		||||
      no_new_session
 | 
			
		||||
      map_real_uid
 | 
			
		||||
      direct_wayland
 | 
			
		||||
      system_bus
 | 
			
		||||
      gpu
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
    session_bus =
 | 
			
		||||
      if session_bus != null then
 | 
			
		||||
        (session_bus (extendSessionDefault id))
 | 
			
		||||
      else
 | 
			
		||||
        (extendSessionDefault id {
 | 
			
		||||
          talk = [ ];
 | 
			
		||||
          own = [ ];
 | 
			
		||||
          call = { };
 | 
			
		||||
          broadcast = { };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    enablements =
 | 
			
		||||
      (if allow_wayland then 1 else 0)
 | 
			
		||||
      + (if allow_x11 then 2 else 0)
 | 
			
		||||
      + (if allow_dbus then 4 else 0)
 | 
			
		||||
      + (if allow_pulse then 8 else 0);
 | 
			
		||||
 | 
			
		||||
    mesa = if gpu then mesaWrappers else null;
 | 
			
		||||
    nix_gl = if gpu then nixGL else null;
 | 
			
		||||
    current_system = nixos.config.system.build.toplevel;
 | 
			
		||||
    activation_package = homeManagerConfiguration.activationPackage;
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
writeScript "fortify-${pname}-bundle-prelude" ''
 | 
			
		||||
  #!${runtimeShell} -el
 | 
			
		||||
  OUT="$(mktemp -d)"
 | 
			
		||||
  TAR="$(mktemp -u)"
 | 
			
		||||
  set -x
 | 
			
		||||
 | 
			
		||||
  nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
 | 
			
		||||
  nix store --store "$OUT" optimise
 | 
			
		||||
  chmod -R +r "$OUT/nix/var"
 | 
			
		||||
  nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
 | 
			
		||||
    "${homeManagerConfiguration.activationPackage}" \
 | 
			
		||||
    "${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
 | 
			
		||||
  mkdir -p "$OUT/etc"
 | 
			
		||||
  tar -C "$OUT/etc" -xf "${etc}/etc.tar"
 | 
			
		||||
  cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
 | 
			
		||||
 | 
			
		||||
  # creating an intermediate file improves zstd performance
 | 
			
		||||
  tar -C "$OUT" -cf "$TAR" .
 | 
			
		||||
  chmod +w -R "$OUT" && rm -rf "$OUT"
 | 
			
		||||
 | 
			
		||||
  zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
 | 
			
		||||
  rm "$TAR"
 | 
			
		||||
''
 | 
			
		||||
							
								
								
									
										83
									
								
								cmd/fpkg/bundle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								cmd/fpkg/bundle.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type bundleInfo struct {
 | 
			
		||||
	Name    string `json:"name"`
 | 
			
		||||
	Version string `json:"version"`
 | 
			
		||||
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	AppID int `json:"app_id"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Groups []string `json:"groups,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	UserNS bool `json:"userns,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Net bool `json:"net,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Dev bool `json:"dev,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	SystemBus *dbus.Config `json:"system_bus,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Enablements system.Enablements `json:"enablements"`
 | 
			
		||||
 | 
			
		||||
	// allow gpu access within sandbox
 | 
			
		||||
	GPU bool `json:"gpu"`
 | 
			
		||||
	// store path to nixGL mesa wrappers
 | 
			
		||||
	Mesa string `json:"mesa,omitempty"`
 | 
			
		||||
	// store path to nixGL source
 | 
			
		||||
	NixGL string `json:"nix_gl,omitempty"`
 | 
			
		||||
	// store path to activate-and-exec script
 | 
			
		||||
	Launcher string `json:"launcher"`
 | 
			
		||||
	// store path to /run/current-system
 | 
			
		||||
	CurrentSystem string `json:"current_system"`
 | 
			
		||||
	// store path to home-manager activation package
 | 
			
		||||
	ActivationPackage string `json:"activation_package"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
 | 
			
		||||
	bundle := new(bundleInfo)
 | 
			
		||||
	if f, err := os.Open(name); err != nil {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatalf("cannot open bundle: %v", err)
 | 
			
		||||
	} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatalf("cannot parse bundle metadata: %v", err)
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		fmsg.Printf("cannot close bundle metadata: %v", err)
 | 
			
		||||
		// not fatal
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if bundle.ID == "" {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatal("application identifier must not be empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bundle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatHostname(name string) string {
 | 
			
		||||
	if h, err := os.Hostname(); err != nil {
 | 
			
		||||
		fmsg.Printf("cannot get hostname: %v", err)
 | 
			
		||||
		return "fortify-" + name
 | 
			
		||||
	} else {
 | 
			
		||||
		return h + "-" + name
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										195
									
								
								cmd/fpkg/install.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								cmd/fpkg/install.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,195 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"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 {
 | 
			
		||||
		fmsg.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
	pkgPath := args[0]
 | 
			
		||||
	if !path.IsAbs(pkgPath) {
 | 
			
		||||
		if dir, err := os.Getwd(); err != nil {
 | 
			
		||||
			fmsg.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 {
 | 
			
		||||
		fmsg.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()
 | 
			
		||||
			fmsg.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}
 | 
			
		||||
		// did not modify app, clean installation condition met later
 | 
			
		||||
	} else if s.IsDir() {
 | 
			
		||||
		cleanup()
 | 
			
		||||
		fmsg.Fatalf("metadata path %q is not a file", pathSet.metaPath)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else {
 | 
			
		||||
		app = loadBundleInfo(pathSet.metaPath, cleanup)
 | 
			
		||||
		if app.ID != bundle.ID {
 | 
			
		||||
			cleanup()
 | 
			
		||||
			fmsg.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()
 | 
			
		||||
			fmsg.Printf("package %q is identical to local application %q", pkgPath, app.ID)
 | 
			
		||||
			fmsg.Exit(0)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// AppID determines uid
 | 
			
		||||
		if app.AppID != bundle.AppID {
 | 
			
		||||
			cleanup()
 | 
			
		||||
			fmsg.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// sec: should compare version string
 | 
			
		||||
		fmsg.VPrintf("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
 | 
			
		||||
	} else {
 | 
			
		||||
		fmsg.VPrintf("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()
 | 
			
		||||
		fmsg.Fatalf("cannot create metadata file: %v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
 | 
			
		||||
		cleanup()
 | 
			
		||||
		fmsg.Fatalf("cannot write metadata: %v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		fmsg.Printf("cannot close metadata file: %v", err)
 | 
			
		||||
		// not fatal
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
 | 
			
		||||
		cleanup()
 | 
			
		||||
		fmsg.Fatalf("cannot rename metadata file: %v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cleanup()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								cmd/fpkg/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								cmd/fpkg/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const shell = "/run/current-system/sw/bin/bash"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	if err := os.Setenv("SHELL", shell); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot set $SHELL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	flagVerbose bool
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	fmsg.SetPrefix("fpkg")
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	fmsg.SetVerbose(flagVerbose)
 | 
			
		||||
 | 
			
		||||
	args := flag.Args()
 | 
			
		||||
	if len(args) < 1 {
 | 
			
		||||
		fmsg.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch args[0] {
 | 
			
		||||
	case "install":
 | 
			
		||||
		actionInstall(args[1:])
 | 
			
		||||
	case "start":
 | 
			
		||||
		actionStart(args[1:])
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		fmsg.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmsg.Exit(0)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								cmd/fpkg/paths.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								cmd/fpkg/paths.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	dataHome string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// dataHome
 | 
			
		||||
	if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
 | 
			
		||||
		dataHome = p
 | 
			
		||||
	} else {
 | 
			
		||||
		dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func lookPath(file string) string {
 | 
			
		||||
	if p, err := exec.LookPath(file); err != nil {
 | 
			
		||||
		fmsg.Fatalf("%s: command not found", file)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else {
 | 
			
		||||
		return p
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var beforeRunFail = new(atomic.Pointer[func()])
 | 
			
		||||
 | 
			
		||||
func mustRun(name string, arg ...string) {
 | 
			
		||||
	fmsg.VPrintf("spawning process: %q %q", name, arg)
 | 
			
		||||
	cmd := exec.Command(name, arg...)
 | 
			
		||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		if f := beforeRunFail.Swap(nil); f != nil {
 | 
			
		||||
			(*f)()
 | 
			
		||||
		}
 | 
			
		||||
		fmsg.Fatalf("%s: %v", name, err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type appPathSet struct {
 | 
			
		||||
	// ${dataHome}/${id}
 | 
			
		||||
	baseDir string
 | 
			
		||||
	// ${baseDir}/app
 | 
			
		||||
	metaPath string
 | 
			
		||||
	// ${baseDir}/files
 | 
			
		||||
	homeDir string
 | 
			
		||||
	// ${baseDir}/cache
 | 
			
		||||
	cacheDir string
 | 
			
		||||
	// ${baseDir}/cache/nix
 | 
			
		||||
	nixPath string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pathSetByApp(id string) *appPathSet {
 | 
			
		||||
	pathSet := new(appPathSet)
 | 
			
		||||
	pathSet.baseDir = path.Join(dataHome, id)
 | 
			
		||||
	pathSet.metaPath = path.Join(pathSet.baseDir, "app")
 | 
			
		||||
	pathSet.homeDir = path.Join(pathSet.baseDir, "files")
 | 
			
		||||
	pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
 | 
			
		||||
	pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
 | 
			
		||||
	return pathSet
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								cmd/fpkg/proc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								cmd/fpkg/proc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func fortifyApp(config *fst.Config, beforeFail func()) {
 | 
			
		||||
	var (
 | 
			
		||||
		cmd *exec.Cmd
 | 
			
		||||
		st  io.WriteCloser
 | 
			
		||||
	)
 | 
			
		||||
	if p, ok := internal.Check(internal.Fortify); !ok {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else if r, w, err := os.Pipe(); err != nil {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatalf("cannot pipe: %v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	} else {
 | 
			
		||||
		if fmsg.Verbose() {
 | 
			
		||||
			cmd = exec.Command(p, "-v", "app", "3")
 | 
			
		||||
		} else {
 | 
			
		||||
			cmd = exec.Command(p, "app", "3")
 | 
			
		||||
		}
 | 
			
		||||
		cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
		cmd.ExtraFiles = []*os.File{r}
 | 
			
		||||
		st = w
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		if err := json.NewEncoder(st).Encode(config); err != nil {
 | 
			
		||||
			beforeFail()
 | 
			
		||||
			fmsg.Fatalf("cannot send configuration: %v", err)
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Start(); err != nil {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Fatalf("cannot start fortify: %v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	}
 | 
			
		||||
	if err := cmd.Wait(); err != nil {
 | 
			
		||||
		var exitError *exec.ExitError
 | 
			
		||||
		if errors.As(err, &exitError) {
 | 
			
		||||
			beforeFail()
 | 
			
		||||
			fmsg.Exit(exitError.ExitCode())
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		} else {
 | 
			
		||||
			beforeFail()
 | 
			
		||||
			fmsg.Fatalf("cannot wait: %v", err)
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										175
									
								
								cmd/fpkg/start.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								cmd/fpkg/start.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
		fmsg.Fatal("invalid argument")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		Parse app metadata.
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	id := args[0]
 | 
			
		||||
	pathSet := pathSetByApp(id)
 | 
			
		||||
	app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
			
		||||
	if app.ID != id {
 | 
			
		||||
		fmsg.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] = shell
 | 
			
		||||
	}
 | 
			
		||||
	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,
 | 
			
		||||
				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() {})
 | 
			
		||||
	fmsg.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},
 | 
			
		||||
	}...)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										98
									
								
								cmd/fpkg/with.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								cmd/fpkg/with.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func withNixDaemon(
 | 
			
		||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
			
		||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
			
		||||
) {
 | 
			
		||||
	fortifyAppDropShell(updateConfig(&fst.Config{
 | 
			
		||||
		ID: app.ID,
 | 
			
		||||
		Command: []string{shell, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
			
		||||
			// start nix-daemon
 | 
			
		||||
			"nix-daemon --store / & " +
 | 
			
		||||
			// wait for socket to appear
 | 
			
		||||
			"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
 | 
			
		||||
			// create directory so nix stops complaining
 | 
			
		||||
			"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
 | 
			
		||||
			strings.Join(command, " && ") +
 | 
			
		||||
			// terminate nix-daemon
 | 
			
		||||
			" && pkill nix-daemon",
 | 
			
		||||
		},
 | 
			
		||||
		Confinement: fst.ConfinementConfig{
 | 
			
		||||
			AppID:    app.AppID,
 | 
			
		||||
			Username: "fortify",
 | 
			
		||||
			Inner:    path.Join("/data/data", app.ID),
 | 
			
		||||
			Outer:    pathSet.homeDir,
 | 
			
		||||
			Sandbox: &fst.SandboxConfig{
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				UserNS:       true, // nix sandbox requires userns
 | 
			
		||||
				Net:          net,
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
			
		||||
				},
 | 
			
		||||
				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},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}), dropShell, beforeFail)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
			
		||||
	fortifyAppDropShell(&fst.Config{
 | 
			
		||||
		ID:      app.ID,
 | 
			
		||||
		Command: []string{shell, "-lc", strings.Join(command, " && ")},
 | 
			
		||||
		Confinement: fst.ConfinementConfig{
 | 
			
		||||
			AppID:    app.AppID,
 | 
			
		||||
			Username: "nixos",
 | 
			
		||||
			Inner:    path.Join("/data/data", app.ID, "cache"),
 | 
			
		||||
			Outer:    pathSet.cacheDir, // this also ensures cacheDir via fshim
 | 
			
		||||
			Sandbox: &fst.SandboxConfig{
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
			
		||||
					{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
			
		||||
				},
 | 
			
		||||
				Link: [][2]string{
 | 
			
		||||
					{app.CurrentSystem, "/run/current-system"},
 | 
			
		||||
					{"/run/current-system/sw/bin", "/bin"},
 | 
			
		||||
					{"/run/current-system/sw/bin", "/usr/bin"},
 | 
			
		||||
				},
 | 
			
		||||
				Etc:     path.Join(workDir, "etc"),
 | 
			
		||||
				AutoEtc: true,
 | 
			
		||||
			},
 | 
			
		||||
			ExtraPerms: []*fst.ExtraPermConfig{
 | 
			
		||||
				{Path: dataHome, Execute: true},
 | 
			
		||||
				{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
			
		||||
				{Path: workDir, Execute: true},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, dropShell, beforeFail)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
 | 
			
		||||
	if dropShell {
 | 
			
		||||
		config.Command = []string{shell, "-l"}
 | 
			
		||||
		fortifyApp(config, beforeFail)
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		fmsg.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
	fortifyApp(config, beforeFail)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										186
									
								
								dbus/address.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								dbus/address.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,186 @@
 | 
			
		||||
package dbus
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AddrEntry struct {
 | 
			
		||||
	Method string      `json:"method"`
 | 
			
		||||
	Values [][2]string `json:"values"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parse parses D-Bus address according to
 | 
			
		||||
// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses
 | 
			
		||||
func Parse(addr []byte) ([]AddrEntry, error) {
 | 
			
		||||
	// Look for a semicolon
 | 
			
		||||
	address := bytes.Split(bytes.TrimSuffix(addr, []byte{';'}), []byte{';'})
 | 
			
		||||
 | 
			
		||||
	// Allocate for entries
 | 
			
		||||
	v := make([]AddrEntry, len(address))
 | 
			
		||||
 | 
			
		||||
	for i, s := range address {
 | 
			
		||||
		var pairs [][]byte
 | 
			
		||||
 | 
			
		||||
		// Look for the colon :
 | 
			
		||||
		if method, list, ok := bytes.Cut(s, []byte{':'}); !ok {
 | 
			
		||||
			return v, &BadAddressError{ErrNoColon, i, s, -1, nil}
 | 
			
		||||
		} else {
 | 
			
		||||
			pairs = bytes.Split(list, []byte{','})
 | 
			
		||||
			v[i].Method = string(method)
 | 
			
		||||
			v[i].Values = make([][2]string, len(pairs))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for j, pair := range pairs {
 | 
			
		||||
			key, value, ok := bytes.Cut(pair, []byte{'='})
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return v, &BadAddressError{ErrBadPairSep, i, s, j, pair}
 | 
			
		||||
			}
 | 
			
		||||
			if len(key) == 0 {
 | 
			
		||||
				return v, &BadAddressError{ErrBadPairKey, i, s, j, pair}
 | 
			
		||||
			}
 | 
			
		||||
			if len(value) == 0 {
 | 
			
		||||
				return v, &BadAddressError{ErrBadPairVal, i, s, j, pair}
 | 
			
		||||
			}
 | 
			
		||||
			v[i].Values[j][0] = string(key)
 | 
			
		||||
 | 
			
		||||
			if val, errno := unescapeValue(value); errno != errSuccess {
 | 
			
		||||
				return v, &BadAddressError{errno, i, s, j, pair}
 | 
			
		||||
			} else {
 | 
			
		||||
				v[i].Values[j][1] = string(val)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func unescapeValue(v []byte) (val []byte, errno ParseError) {
 | 
			
		||||
	if l := len(v) - (bytes.Count(v, []byte{'%'}) * 2); l < 0 {
 | 
			
		||||
		errno = ErrBadValLength
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		val = make([]byte, l)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var i, skip int
 | 
			
		||||
	for iu, b := range v {
 | 
			
		||||
		if skip > 0 {
 | 
			
		||||
			skip--
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
 | 
			
		||||
			goto opt
 | 
			
		||||
		} else if b >= '0' && b <= '9' { // 0-9
 | 
			
		||||
			goto opt
 | 
			
		||||
		} else if b >= 'A' && b <= 'Z' { // A-Z
 | 
			
		||||
			goto opt
 | 
			
		||||
		} else if b >= 'a' && b <= 'z' { // a-z
 | 
			
		||||
			goto opt
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b != '%' {
 | 
			
		||||
			errno = ErrBadValByte
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		skip += 2
 | 
			
		||||
		if iu+2 >= len(v) {
 | 
			
		||||
			errno = ErrBadValHexLength
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
 | 
			
		||||
			if errors.As(err, new(hex.InvalidByteError)) {
 | 
			
		||||
				errno = ErrBadValHexByte
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			// unreachable
 | 
			
		||||
			panic(err.Error())
 | 
			
		||||
		} else if c != 1 {
 | 
			
		||||
			// unreachable
 | 
			
		||||
			panic(fmt.Sprintf("invalid decode length %d", c))
 | 
			
		||||
		}
 | 
			
		||||
		i++
 | 
			
		||||
		continue
 | 
			
		||||
 | 
			
		||||
	opt:
 | 
			
		||||
		val[i] = b
 | 
			
		||||
		i++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ParseError uint8
 | 
			
		||||
 | 
			
		||||
func (e ParseError) Error() string {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case errSuccess:
 | 
			
		||||
		panic("attempted to return success as error")
 | 
			
		||||
	case ErrNoColon:
 | 
			
		||||
		return "address does not contain a colon"
 | 
			
		||||
	case ErrBadPairSep:
 | 
			
		||||
		return "'=' character not found"
 | 
			
		||||
	case ErrBadPairKey:
 | 
			
		||||
		return "'=' character has no key preceding it"
 | 
			
		||||
	case ErrBadPairVal:
 | 
			
		||||
		return "'=' character has no value following it"
 | 
			
		||||
	case ErrBadValLength:
 | 
			
		||||
		return "unescaped value has impossible length"
 | 
			
		||||
	case ErrBadValByte:
 | 
			
		||||
		return "in D-Bus address, characters other than [-0-9A-Za-z_/.\\*] should have been escaped"
 | 
			
		||||
	case ErrBadValHexLength:
 | 
			
		||||
		return "in D-Bus address, percent character was not followed by two hex digits"
 | 
			
		||||
	case ErrBadValHexByte:
 | 
			
		||||
		return "in D-Bus address, percent character was followed by characters other than hex digits"
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Sprintf("parse error %d", e)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	errSuccess ParseError = iota
 | 
			
		||||
	ErrNoColon
 | 
			
		||||
	ErrBadPairSep
 | 
			
		||||
	ErrBadPairKey
 | 
			
		||||
	ErrBadPairVal
 | 
			
		||||
	ErrBadValLength
 | 
			
		||||
	ErrBadValByte
 | 
			
		||||
	ErrBadValHexLength
 | 
			
		||||
	ErrBadValHexByte
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BadAddressError struct {
 | 
			
		||||
	// error type
 | 
			
		||||
	Type ParseError
 | 
			
		||||
 | 
			
		||||
	// bad entry position
 | 
			
		||||
	EntryPos int
 | 
			
		||||
	// bad entry value
 | 
			
		||||
	EntryVal []byte
 | 
			
		||||
 | 
			
		||||
	// bad pair position
 | 
			
		||||
	PairPos int
 | 
			
		||||
	// bad pair value
 | 
			
		||||
	PairVal []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *BadAddressError) Is(err error) bool {
 | 
			
		||||
	var b *BadAddressError
 | 
			
		||||
	return errors.As(err, &b) && a.Type == b.Type &&
 | 
			
		||||
		a.EntryPos == b.EntryPos && slices.Equal(a.EntryVal, b.EntryVal) &&
 | 
			
		||||
		a.PairPos == b.PairPos && slices.Equal(a.PairVal, b.PairVal)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *BadAddressError) Error() string {
 | 
			
		||||
	return a.Type.Error()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *BadAddressError) Unwrap() error {
 | 
			
		||||
	return a.Type
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								dbus/address_escape_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								dbus/address_escape_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
package dbus
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestUnescapeValue(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		value   string
 | 
			
		||||
		want    string
 | 
			
		||||
		wantErr ParseError
 | 
			
		||||
	}{
 | 
			
		||||
		// upstream test cases
 | 
			
		||||
		{value: "abcde", want: "abcde"},
 | 
			
		||||
		{value: "", want: ""},
 | 
			
		||||
		{value: "%20%20", want: "  "},
 | 
			
		||||
		{value: "%24", want: "$"},
 | 
			
		||||
		{value: "%25", want: "%"},
 | 
			
		||||
		{value: "abc%24", want: "abc$"},
 | 
			
		||||
		{value: "%24abc", want: "$abc"},
 | 
			
		||||
		{value: "abc%24abc", want: "abc$abc"},
 | 
			
		||||
		{value: "/", want: "/"},
 | 
			
		||||
		{value: "-", want: "-"},
 | 
			
		||||
		{value: "_", want: "_"},
 | 
			
		||||
		{value: "A", want: "A"},
 | 
			
		||||
		{value: "I", want: "I"},
 | 
			
		||||
		{value: "Z", want: "Z"},
 | 
			
		||||
		{value: "a", want: "a"},
 | 
			
		||||
		{value: "i", want: "i"},
 | 
			
		||||
		{value: "z", want: "z"},
 | 
			
		||||
		/* Bug: https://bugs.freedesktop.org/show_bug.cgi?id=53499 */
 | 
			
		||||
		{value: "%c3%b6", want: "\xc3\xb6"},
 | 
			
		||||
 | 
			
		||||
		{value: "%a", wantErr: ErrBadValHexLength},
 | 
			
		||||
		{value: "%q", wantErr: ErrBadValHexLength},
 | 
			
		||||
		{value: "%az", wantErr: ErrBadValHexByte},
 | 
			
		||||
		{value: "%%", wantErr: ErrBadValLength},
 | 
			
		||||
		{value: "%$$", wantErr: ErrBadValHexByte},
 | 
			
		||||
		{value: "abc%a", wantErr: ErrBadValHexLength},
 | 
			
		||||
		{value: "%axyz", wantErr: ErrBadValHexByte},
 | 
			
		||||
		{value: "%", wantErr: ErrBadValLength},
 | 
			
		||||
		{value: "$", wantErr: ErrBadValByte},
 | 
			
		||||
		{value: " ", wantErr: ErrBadValByte},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run("unescape "+tc.value, func(t *testing.T) {
 | 
			
		||||
			if got, errno := unescapeValue([]byte(tc.value)); errno != tc.wantErr {
 | 
			
		||||
				t.Errorf("unescapeValue() errno = %v, wantErr %v", errno, tc.wantErr)
 | 
			
		||||
			} else if tc.wantErr == errSuccess && string(got) != tc.want {
 | 
			
		||||
				t.Errorf("unescapeValue() = %q, want %q", got, tc.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										119
									
								
								dbus/address_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								dbus/address_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
package dbus_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParse(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		addr    string
 | 
			
		||||
		want    []dbus.AddrEntry
 | 
			
		||||
		wantErr error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "simple session unix",
 | 
			
		||||
			addr: "unix:path=/run/user/1971/bus",
 | 
			
		||||
			want: []dbus.AddrEntry{{
 | 
			
		||||
				Method: "unix",
 | 
			
		||||
				Values: [][2]string{{"path", "/run/user/1971/bus"}},
 | 
			
		||||
			}},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "simple upper escape",
 | 
			
		||||
			addr: "debug:name=Test,cat=cute,escaped=%c3%b6",
 | 
			
		||||
			want: []dbus.AddrEntry{{
 | 
			
		||||
				Method: "debug",
 | 
			
		||||
				Values: [][2]string{
 | 
			
		||||
					{"name", "Test"},
 | 
			
		||||
					{"cat", "cute"},
 | 
			
		||||
					{"escaped", "\xc3\xb6"},
 | 
			
		||||
				},
 | 
			
		||||
			}},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "simple bad escape",
 | 
			
		||||
			addr: "debug:name=%",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadValLength,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("debug:name=%"), PairPos: 0, PairVal: []byte("name=%")},
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// upstream test cases
 | 
			
		||||
		{
 | 
			
		||||
			name: "full address success",
 | 
			
		||||
			addr: "unix:path=/tmp/foo;debug:name=test,sliff=sloff;",
 | 
			
		||||
			want: []dbus.AddrEntry{
 | 
			
		||||
				{Method: "unix", Values: [][2]string{{"path", "/tmp/foo"}}},
 | 
			
		||||
				{Method: "debug", Values: [][2]string{{"name", "test"}, {"sliff", "sloff"}}},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty address",
 | 
			
		||||
			addr: "",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
 | 
			
		||||
				EntryVal: []byte{}, PairPos: -1},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no body",
 | 
			
		||||
			addr: "foo",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo"), PairPos: -1},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no pair separator",
 | 
			
		||||
			addr: "foo:bar",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:bar"), PairPos: 0, PairVal: []byte("bar")},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no pair separator multi pair",
 | 
			
		||||
			addr: "foo:bar,baz",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:bar,baz"), PairPos: 0, PairVal: []byte("bar")},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no pair separator single valid pair",
 | 
			
		||||
			addr: "foo:bar=foo,baz",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:bar=foo,baz"), PairPos: 1, PairVal: []byte("baz")},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no body single valid address",
 | 
			
		||||
			addr: "foo:bar=foo;baz",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
 | 
			
		||||
				EntryPos: 1, EntryVal: []byte("baz"), PairPos: -1},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no key",
 | 
			
		||||
			addr: "foo:=foo",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairKey,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:=foo"), PairPos: 0, PairVal: []byte("=foo")},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no value",
 | 
			
		||||
			addr: "foo:foo=",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairVal,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:foo="), PairPos: 0, PairVal: []byte("foo=")},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no pair separator single valid pair trailing",
 | 
			
		||||
			addr: "foo:foo,bar=baz",
 | 
			
		||||
			wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
 | 
			
		||||
				EntryPos: 0, EntryVal: []byte("foo:foo,bar=baz"), PairPos: 0, PairVal: []byte("foo")},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			if got, err := dbus.Parse([]byte(tc.addr)); !errors.Is(err, tc.wantErr) {
 | 
			
		||||
				t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
 | 
			
		||||
			} else if tc.wantErr == nil && !reflect.DeepEqual(got, tc.want) {
 | 
			
		||||
				t.Errorf("Parse() = %#v, want %#v", got, tc.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -13,7 +13,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestConfig_Args(t *testing.T) {
 | 
			
		||||
	for _, tc := range testCases() {
 | 
			
		||||
	for _, tc := range makeTestCases() {
 | 
			
		||||
		if tc.wantErr {
 | 
			
		||||
			// args does not check for nulls
 | 
			
		||||
			continue
 | 
			
		||||
@ -30,7 +30,7 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewConfigFromFile(t *testing.T) {
 | 
			
		||||
	for _, tc := range testCases() {
 | 
			
		||||
	for _, tc := range makeTestCases() {
 | 
			
		||||
		name := new(strings.Builder)
 | 
			
		||||
		name.WriteString("parse configuration file for application ")
 | 
			
		||||
		name.WriteString(tc.id)
 | 
			
		||||
 | 
			
		||||
@ -82,10 +82,10 @@ var samples = []dbusTestCase{
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		"moe.ophivana.CrashTestDummy", &dbus.Config{
 | 
			
		||||
			See:       []string{"moe.ophivana.CrashTestDummy1"},
 | 
			
		||||
		"uk.gensokyo.CrashTestDummy", &dbus.Config{
 | 
			
		||||
			See:       []string{"uk.gensokyo.CrashTestDummy1"},
 | 
			
		||||
			Talk:      []string{"org.freedesktop.Notifications"},
 | 
			
		||||
			Own:       []string{"moe.ophivana.CrashTestDummy.*", "org.mpris.MediaPlayer2.moe.ophivana.CrashTestDummy.*"},
 | 
			
		||||
			Own:       []string{"uk.gensokyo.CrashTestDummy.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"},
 | 
			
		||||
			Call:      map[string]string{"org.freedesktop.portal.*": "*"},
 | 
			
		||||
			Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
 | 
			
		||||
			Log:       true,
 | 
			
		||||
@ -96,19 +96,19 @@ var samples = []dbusTestCase{
 | 
			
		||||
			"unix:path=/run/user/1971/bus",
 | 
			
		||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
			
		||||
			"--filter",
 | 
			
		||||
			"--see=moe.ophivana.CrashTestDummy1",
 | 
			
		||||
			"--see=uk.gensokyo.CrashTestDummy1",
 | 
			
		||||
			"--talk=org.freedesktop.Notifications",
 | 
			
		||||
			"--own=moe.ophivana.CrashTestDummy.*",
 | 
			
		||||
			"--own=org.mpris.MediaPlayer2.moe.ophivana.CrashTestDummy.*",
 | 
			
		||||
			"--own=uk.gensokyo.CrashTestDummy.*",
 | 
			
		||||
			"--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*",
 | 
			
		||||
			"--call=org.freedesktop.portal.*=*",
 | 
			
		||||
			"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
 | 
			
		||||
			"--log"},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		"moe.ophivana.CrashTestDummy1", &dbus.Config{
 | 
			
		||||
			See:       []string{"moe.ophivana.CrashTestDummy"},
 | 
			
		||||
		"uk.gensokyo.CrashTestDummy1", &dbus.Config{
 | 
			
		||||
			See:       []string{"uk.gensokyo.CrashTestDummy"},
 | 
			
		||||
			Talk:      []string{"org.freedesktop.Notifications"},
 | 
			
		||||
			Own:       []string{"moe.ophivana.CrashTestDummy1.*", "org.mpris.MediaPlayer2.moe.ophivana.CrashTestDummy1.*"},
 | 
			
		||||
			Own:       []string{"uk.gensokyo.CrashTestDummy1.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*"},
 | 
			
		||||
			Call:      map[string]string{"org.freedesktop.portal.*": "*"},
 | 
			
		||||
			Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
 | 
			
		||||
			Log:       true,
 | 
			
		||||
@ -119,10 +119,10 @@ var samples = []dbusTestCase{
 | 
			
		||||
			"unix:path=/run/user/1971/bus",
 | 
			
		||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
			
		||||
			"--filter",
 | 
			
		||||
			"--see=moe.ophivana.CrashTestDummy",
 | 
			
		||||
			"--see=uk.gensokyo.CrashTestDummy",
 | 
			
		||||
			"--talk=org.freedesktop.Notifications",
 | 
			
		||||
			"--own=moe.ophivana.CrashTestDummy1.*",
 | 
			
		||||
			"--own=org.mpris.MediaPlayer2.moe.ophivana.CrashTestDummy1.*",
 | 
			
		||||
			"--own=uk.gensokyo.CrashTestDummy1.*",
 | 
			
		||||
			"--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*",
 | 
			
		||||
			"--call=org.freedesktop.portal.*=*",
 | 
			
		||||
			"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
 | 
			
		||||
			"--log"},
 | 
			
		||||
@ -145,7 +145,7 @@ var (
 | 
			
		||||
	testCaseOnce sync.Once
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func testCases() []dbusTestCase {
 | 
			
		||||
func makeTestCases() []dbusTestCase {
 | 
			
		||||
	testCaseOnce.Do(testCaseGenerate)
 | 
			
		||||
	return testCasesV
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  "see": [
 | 
			
		||||
    "moe.ophivana.CrashTestDummy1"
 | 
			
		||||
    "uk.gensokyo.CrashTestDummy1"
 | 
			
		||||
  ],
 | 
			
		||||
  "talk":[
 | 
			
		||||
    "org.freedesktop.Notifications"
 | 
			
		||||
  ],
 | 
			
		||||
  "own":[
 | 
			
		||||
    "moe.ophivana.CrashTestDummy.*",
 | 
			
		||||
    "org.mpris.MediaPlayer2.moe.ophivana.CrashTestDummy.*"
 | 
			
		||||
    "uk.gensokyo.CrashTestDummy.*",
 | 
			
		||||
    "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"
 | 
			
		||||
  ],
 | 
			
		||||
  "call":{
 | 
			
		||||
    "org.freedesktop.portal.*":"*"
 | 
			
		||||
							
								
								
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							@ -2,6 +2,8 @@
 | 
			
		||||
cd "$(dirname -- "$0")" || exit 1
 | 
			
		||||
 | 
			
		||||
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
 | 
			
		||||
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
 | 
			
		||||
 | 
			
		||||
install -vDm0755 "bin/fshim" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fshim"
 | 
			
		||||
install -vDm0755 "bin/finit" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/finit"
 | 
			
		||||
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								dist/release.sh
									
									
									
									
										vendored
									
									
								
							@ -10,6 +10,7 @@ cp -rv "comp" "${out}"
 | 
			
		||||
 | 
			
		||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Fortify=/usr/bin/fortify
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
 | 
			
		||||
  -X git.gensokyo.uk/security/fortify/internal.Finit=/usr/libexec/fortify/finit
 | 
			
		||||
  -X main.Fmain=/usr/bin/fortify
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@ -7,11 +7,11 @@
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1733951536,
 | 
			
		||||
        "narHash": "sha256-Zb5ZCa7Xj+0gy5XVXINTSr71fCfAv+IKtmIXNrykT54=",
 | 
			
		||||
        "lastModified": 1735344290,
 | 
			
		||||
        "narHash": "sha256-oJDtWPH1oJT34RJK1FSWjwX4qcGOBRkcNQPD0EbSfNM=",
 | 
			
		||||
        "owner": "nix-community",
 | 
			
		||||
        "repo": "home-manager",
 | 
			
		||||
        "rev": "1318c3f3b068cdcea922fa7c1a0a1f0c96c22f5f",
 | 
			
		||||
        "rev": "613691f285dad87694c2ba1c9e6298d04736292d",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
@ -23,11 +23,11 @@
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1734298236,
 | 
			
		||||
        "narHash": "sha256-aWhhqY44xBjMoO9r5fyPp5u8tqUNWRZ/m/P+abMSs5c=",
 | 
			
		||||
        "lastModified": 1735326919,
 | 
			
		||||
        "narHash": "sha256-BZlgs4l9CXAauo78giGCZdazMMk5VZNro7o5SHFUuyE=",
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "eb919d9300b6a18f8583f58aef16db458fbd7bec",
 | 
			
		||||
        "rev": "8f0aa155aa29f7d2b471aa2ffd322745bf2b2036",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								flake.nix
									
									
									
									
									
								
							@ -29,6 +29,20 @@
 | 
			
		||||
    {
 | 
			
		||||
      nixosModules.fortify = import ./nixos.nix;
 | 
			
		||||
 | 
			
		||||
      buildPackage = forAllSystems (
 | 
			
		||||
        system:
 | 
			
		||||
        nixpkgsFor.${system}.callPackage (
 | 
			
		||||
          import ./bundle.nix {
 | 
			
		||||
            inherit
 | 
			
		||||
              nixpkgsFor
 | 
			
		||||
              system
 | 
			
		||||
              nixpkgs
 | 
			
		||||
              home-manager
 | 
			
		||||
              ;
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      checks = forAllSystems (
 | 
			
		||||
        system:
 | 
			
		||||
        let
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										125
									
								
								fst/config.go
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								fst/config.go
									
									
									
									
									
								
							@ -1,11 +1,7 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -49,37 +45,6 @@ type ConfinementConfig struct {
 | 
			
		||||
	Enablements system.Enablements `json:"enablements"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SandboxConfig describes resources made available to the sandbox.
 | 
			
		||||
type SandboxConfig struct {
 | 
			
		||||
	// unix hostname within sandbox
 | 
			
		||||
	Hostname string `json:"hostname,omitempty"`
 | 
			
		||||
	// allow userns within sandbox
 | 
			
		||||
	UserNS bool `json:"userns,omitempty"`
 | 
			
		||||
	// share net namespace
 | 
			
		||||
	Net bool `json:"net,omitempty"`
 | 
			
		||||
	// share all devices
 | 
			
		||||
	Dev bool `json:"dev,omitempty"`
 | 
			
		||||
	// 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
 | 
			
		||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// final environment variables
 | 
			
		||||
	Env map[string]string `json:"env"`
 | 
			
		||||
	// sandbox host filesystem access
 | 
			
		||||
	Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
			
		||||
	// symlinks created inside the sandbox
 | 
			
		||||
	Link [][2]string `json:"symlink"`
 | 
			
		||||
	// read-only /etc directory
 | 
			
		||||
	Etc string `json:"etc,omitempty"`
 | 
			
		||||
	// automatically set up /etc symlinks
 | 
			
		||||
	AutoEtc bool `json:"auto_etc"`
 | 
			
		||||
	// paths to override by mounting tmpfs over them
 | 
			
		||||
	Override []string `json:"override"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExtraPermConfig struct {
 | 
			
		||||
	Ensure  bool   `json:"ensure,omitempty"`
 | 
			
		||||
	Path    string `json:"path"`
 | 
			
		||||
@ -121,96 +86,6 @@ type FilesystemConfig struct {
 | 
			
		||||
	Must bool `json:"require,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
 | 
			
		||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
 | 
			
		||||
func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return nil, errors.New("nil sandbox config")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var uid int
 | 
			
		||||
	if !s.MapRealUID {
 | 
			
		||||
		uid = 65534
 | 
			
		||||
	} else {
 | 
			
		||||
		uid = os.Geteuid()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf := (&bwrap.Config{
 | 
			
		||||
		Net:           s.Net,
 | 
			
		||||
		UserNS:        s.UserNS,
 | 
			
		||||
		Hostname:      s.Hostname,
 | 
			
		||||
		Clearenv:      true,
 | 
			
		||||
		SetEnv:        s.Env,
 | 
			
		||||
		NewSession:    !s.NoNewSession,
 | 
			
		||||
		DieWithParent: true,
 | 
			
		||||
		AsInit:        true,
 | 
			
		||||
 | 
			
		||||
		// initialise map
 | 
			
		||||
		Chmod: make(bwrap.ChmodConfig),
 | 
			
		||||
	}).
 | 
			
		||||
		SetUID(uid).SetGID(uid).
 | 
			
		||||
		Procfs("/proc").
 | 
			
		||||
		Tmpfs(Tmp, 4*1024)
 | 
			
		||||
 | 
			
		||||
	if !s.Dev {
 | 
			
		||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
			
		||||
	} else {
 | 
			
		||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !s.AutoEtc {
 | 
			
		||||
		if s.Etc == "" {
 | 
			
		||||
			conf.Dir("/etc")
 | 
			
		||||
		} else {
 | 
			
		||||
			conf.Bind(s.Etc, "/etc")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range s.Filesystem {
 | 
			
		||||
		if c == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		src := c.Src
 | 
			
		||||
		dest := c.Dst
 | 
			
		||||
		if c.Dst == "" {
 | 
			
		||||
			dest = c.Src
 | 
			
		||||
		}
 | 
			
		||||
		conf.Bind(src, dest, !c.Must, c.Write, c.Device)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, l := range s.Link {
 | 
			
		||||
		conf.Symlink(l[0], l[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.AutoEtc {
 | 
			
		||||
		etc := s.Etc
 | 
			
		||||
		if etc == "" {
 | 
			
		||||
			etc = "/etc"
 | 
			
		||||
		}
 | 
			
		||||
		conf.Bind(etc, Tmp+"/etc")
 | 
			
		||||
 | 
			
		||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
			
		||||
		if d, err := os.ReadDir(etc); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, ent := range d {
 | 
			
		||||
				name := ent.Name()
 | 
			
		||||
				switch name {
 | 
			
		||||
				case "passwd":
 | 
			
		||||
				case "group":
 | 
			
		||||
 | 
			
		||||
				case "mtab":
 | 
			
		||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
			
		||||
				default:
 | 
			
		||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return conf, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Template returns a fully populated instance of Config.
 | 
			
		||||
func Template() *Config {
 | 
			
		||||
	return &Config{
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								fst/info.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								fst/info.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
type Info struct {
 | 
			
		||||
	User int `json:"user"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								fst/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								fst/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func deepContainsH(basepath, targpath string) (bool, error) {
 | 
			
		||||
	rel, err := filepath.Rel(basepath, targpath)
 | 
			
		||||
	return err == nil && rel != ".." && !strings.HasPrefix(rel, string([]byte{'.', '.', filepath.Separator})), err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								fst/path_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								fst/path_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDeepContainsH(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		basepath string
 | 
			
		||||
		targpath string
 | 
			
		||||
		want     bool
 | 
			
		||||
		wantErr  bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty",
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "equal abs",
 | 
			
		||||
			basepath: "/run",
 | 
			
		||||
			targpath: "/run",
 | 
			
		||||
			want:     true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "equal rel",
 | 
			
		||||
			basepath: "./run",
 | 
			
		||||
			targpath: "run",
 | 
			
		||||
			want:     true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "contains abs",
 | 
			
		||||
			basepath: "/run",
 | 
			
		||||
			targpath: "/run/dbus",
 | 
			
		||||
			want:     true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "inverse contains abs",
 | 
			
		||||
			basepath: "/run/dbus",
 | 
			
		||||
			targpath: "/run",
 | 
			
		||||
			want:     false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "contains rel",
 | 
			
		||||
			basepath: "../run",
 | 
			
		||||
			targpath: "../run/dbus",
 | 
			
		||||
			want:     true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "inverse contains rel",
 | 
			
		||||
			basepath: "../run/dbus",
 | 
			
		||||
			targpath: "../run",
 | 
			
		||||
			want:     false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "weird abs",
 | 
			
		||||
			basepath: "/run/dbus",
 | 
			
		||||
			targpath: "/run/dbus/../current-system",
 | 
			
		||||
			want:     false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "weird rel",
 | 
			
		||||
			basepath: "../run/dbus",
 | 
			
		||||
			targpath: "../run/dbus/../current-system",
 | 
			
		||||
			want:     false,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			name:     "invalid mix",
 | 
			
		||||
			basepath: "/run",
 | 
			
		||||
			targpath: "./run",
 | 
			
		||||
			wantErr:  true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			if got, err := deepContainsH(tc.basepath, tc.targpath); (err != nil) != tc.wantErr {
 | 
			
		||||
				t.Errorf("deepContainsH() error = %v, wantErr %v", err, tc.wantErr)
 | 
			
		||||
			} else if got != tc.want {
 | 
			
		||||
				t.Errorf("deepContainsH() = %v, want %v", got, tc.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										222
									
								
								fst/sandbox.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								fst/sandbox.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,222 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SandboxConfig describes resources made available to the sandbox.
 | 
			
		||||
type SandboxConfig struct {
 | 
			
		||||
	// unix hostname within sandbox
 | 
			
		||||
	Hostname string `json:"hostname,omitempty"`
 | 
			
		||||
	// allow userns within sandbox
 | 
			
		||||
	UserNS bool `json:"userns,omitempty"`
 | 
			
		||||
	// share net namespace
 | 
			
		||||
	Net bool `json:"net,omitempty"`
 | 
			
		||||
	// share all devices
 | 
			
		||||
	Dev bool `json:"dev,omitempty"`
 | 
			
		||||
	// 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
 | 
			
		||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// final environment variables
 | 
			
		||||
	Env map[string]string `json:"env"`
 | 
			
		||||
	// sandbox host filesystem access
 | 
			
		||||
	Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
			
		||||
	// symlinks created inside the sandbox
 | 
			
		||||
	Link [][2]string `json:"symlink"`
 | 
			
		||||
	// read-only /etc directory
 | 
			
		||||
	Etc string `json:"etc,omitempty"`
 | 
			
		||||
	// automatically set up /etc symlinks
 | 
			
		||||
	AutoEtc bool `json:"auto_etc"`
 | 
			
		||||
	// paths to override by mounting tmpfs over them
 | 
			
		||||
	Override []string `json:"override"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
 | 
			
		||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
 | 
			
		||||
func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return nil, errors.New("nil sandbox config")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var uid int
 | 
			
		||||
	if !s.MapRealUID {
 | 
			
		||||
		uid = 65534
 | 
			
		||||
	} else {
 | 
			
		||||
		uid = os.Geteuid()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf := (&bwrap.Config{
 | 
			
		||||
		Net:      s.Net,
 | 
			
		||||
		UserNS:   s.UserNS,
 | 
			
		||||
		Hostname: s.Hostname,
 | 
			
		||||
		Clearenv: true,
 | 
			
		||||
		SetEnv:   s.Env,
 | 
			
		||||
 | 
			
		||||
		/* this is only 4 KiB of memory on a 64-bit system,
 | 
			
		||||
		permissive defaults on NixOS results in around 100 entries
 | 
			
		||||
		so this capacity should eliminate copies for most setups */
 | 
			
		||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
			
		||||
 | 
			
		||||
		NewSession:    !s.NoNewSession,
 | 
			
		||||
		DieWithParent: true,
 | 
			
		||||
		AsInit:        true,
 | 
			
		||||
 | 
			
		||||
		// initialise unconditionally as Once cannot be justified
 | 
			
		||||
		// for saving such a miniscule amount of memory
 | 
			
		||||
		Chmod: make(bwrap.ChmodConfig),
 | 
			
		||||
	}).
 | 
			
		||||
		SetUID(uid).SetGID(uid).
 | 
			
		||||
		Procfs("/proc").
 | 
			
		||||
		Tmpfs(Tmp, 4*1024)
 | 
			
		||||
 | 
			
		||||
	if !s.Dev {
 | 
			
		||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
			
		||||
	} else {
 | 
			
		||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !s.AutoEtc {
 | 
			
		||||
		if s.Etc == "" {
 | 
			
		||||
			conf.Dir("/etc")
 | 
			
		||||
		} else {
 | 
			
		||||
			conf.Bind(s.Etc, "/etc")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// retrieve paths and hide them if they're made available in the sandbox
 | 
			
		||||
	var hidePaths []string
 | 
			
		||||
	sc := os.Paths()
 | 
			
		||||
	hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
			
		||||
	_, systemBusAddr := dbus.Address()
 | 
			
		||||
	if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		// there is usually only one, do not preallocate
 | 
			
		||||
		for _, entry := range entries {
 | 
			
		||||
			if entry.Method != "unix" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			for _, pair := range entry.Values {
 | 
			
		||||
				if pair[0] == "path" {
 | 
			
		||||
					if path.IsAbs(pair[1]) {
 | 
			
		||||
						// get parent dir of socket
 | 
			
		||||
						dir := path.Dir(pair[1])
 | 
			
		||||
						if dir == "." || dir == "/" {
 | 
			
		||||
							fmsg.VPrintf("dbus socket %q is in an unusual location", pair[1])
 | 
			
		||||
						}
 | 
			
		||||
						hidePaths = append(hidePaths, dir)
 | 
			
		||||
					} else {
 | 
			
		||||
						fmsg.VPrintf("dbus socket %q is not absolute", pair[1])
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	hidePathMatch := make([]bool, len(hidePaths))
 | 
			
		||||
	for i := range hidePaths {
 | 
			
		||||
		if err := evalSymlinks(os, &hidePaths[i]); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range s.Filesystem {
 | 
			
		||||
		if c == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !path.IsAbs(c.Src) {
 | 
			
		||||
			return nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dest := c.Dst
 | 
			
		||||
		if c.Dst == "" {
 | 
			
		||||
			dest = c.Src
 | 
			
		||||
		} else if !path.IsAbs(dest) {
 | 
			
		||||
			return nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		srcH := c.Src
 | 
			
		||||
		if err := evalSymlinks(os, &srcH); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for i := range hidePaths {
 | 
			
		||||
			// skip matched entries
 | 
			
		||||
			if hidePathMatch[i] {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			} else if ok {
 | 
			
		||||
				hidePathMatch[i] = true
 | 
			
		||||
				fmsg.VPrintf("hiding paths from %q", c.Src)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// hide marked paths before setting up shares
 | 
			
		||||
	for i, ok := range hidePathMatch {
 | 
			
		||||
		if ok {
 | 
			
		||||
			conf.Tmpfs(hidePaths[i], 8192)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, l := range s.Link {
 | 
			
		||||
		conf.Symlink(l[0], l[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.AutoEtc {
 | 
			
		||||
		etc := s.Etc
 | 
			
		||||
		if etc == "" {
 | 
			
		||||
			etc = "/etc"
 | 
			
		||||
		}
 | 
			
		||||
		conf.Bind(etc, Tmp+"/etc")
 | 
			
		||||
 | 
			
		||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
			
		||||
		if d, err := os.ReadDir(etc); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, ent := range d {
 | 
			
		||||
				name := ent.Name()
 | 
			
		||||
				switch name {
 | 
			
		||||
				case "passwd":
 | 
			
		||||
				case "group":
 | 
			
		||||
 | 
			
		||||
				case "mtab":
 | 
			
		||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
			
		||||
				default:
 | 
			
		||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return conf, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func evalSymlinks(os linux.System, v *string) error {
 | 
			
		||||
	if p, err := os.EvalSymlinks(*v); err != nil {
 | 
			
		||||
		if !errors.Is(err, fs.ErrNotExist) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		fmsg.VPrintf("path %q does not yet exist", *v)
 | 
			
		||||
	} else {
 | 
			
		||||
		*v = p
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -20,7 +20,7 @@ type bubblewrap struct {
 | 
			
		||||
	name string
 | 
			
		||||
 | 
			
		||||
	// bwrap pipes
 | 
			
		||||
	p *pipes
 | 
			
		||||
	control *pipes
 | 
			
		||||
	// sync pipe
 | 
			
		||||
	sync *os.File
 | 
			
		||||
	// returns an array of arguments passed directly
 | 
			
		||||
@ -29,7 +29,7 @@ type bubblewrap struct {
 | 
			
		||||
 | 
			
		||||
	// pipes received by the child
 | 
			
		||||
	// nil if no pipes are required
 | 
			
		||||
	cp *pipes
 | 
			
		||||
	controlPt *pipes
 | 
			
		||||
 | 
			
		||||
	lock sync.RWMutex
 | 
			
		||||
	*exec.Cmd
 | 
			
		||||
@ -39,7 +39,7 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
	b.lock.Lock()
 | 
			
		||||
	defer b.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	if ready != nil && b.cp == nil {
 | 
			
		||||
	if ready != nil && b.controlPt == nil {
 | 
			
		||||
		panic("attempted to start with status monitoring on a bwrap child initialised without pipes")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -50,16 +50,16 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// prepare bwrap pipe and args
 | 
			
		||||
	if argsFD, _, err := b.p.prepareCmd(b.Cmd); err != nil {
 | 
			
		||||
	if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// prepare child args and pipes if enabled
 | 
			
		||||
	if b.cp != nil {
 | 
			
		||||
		b.cp.ready = ready
 | 
			
		||||
		if argsFD, statFD, err := b.cp.prepareCmd(b.Cmd); err != nil {
 | 
			
		||||
	if b.controlPt != nil {
 | 
			
		||||
		b.controlPt.ready = ready
 | 
			
		||||
		if argsFD, statFD, err := b.controlPt.prepareCmd(b.Cmd); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Cmd.Args = append(b.Cmd.Args, b.argF(argsFD, statFD)...)
 | 
			
		||||
@ -70,7 +70,7 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
 | 
			
		||||
	if ready != nil {
 | 
			
		||||
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1")
 | 
			
		||||
	} else if b.cp != nil {
 | 
			
		||||
	} else if b.controlPt != nil {
 | 
			
		||||
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0")
 | 
			
		||||
	} else {
 | 
			
		||||
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
 | 
			
		||||
@ -85,13 +85,13 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write bwrap args first
 | 
			
		||||
	if err := b.p.readyWriteArgs(); err != nil {
 | 
			
		||||
	if err := b.control.readyWriteArgs(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write child args if enabled
 | 
			
		||||
	if b.cp != nil {
 | 
			
		||||
		if err := b.cp.readyWriteArgs(); err != nil {
 | 
			
		||||
	if b.controlPt != nil {
 | 
			
		||||
		if err := b.controlPt.readyWriteArgs(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -100,11 +100,11 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *bubblewrap) Close() error {
 | 
			
		||||
	if b.cp == nil {
 | 
			
		||||
	if b.controlPt == nil {
 | 
			
		||||
		panic("attempted to close bwrap child initialised without pipes")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.cp.closeStatus()
 | 
			
		||||
	return b.controlPt.closeStatus()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *bubblewrap) Start() error {
 | 
			
		||||
@ -136,14 +136,14 @@ func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD,
 | 
			
		||||
	if args, err := NewCheckedArgs(conf.Args()); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		b.p = &pipes{args: args}
 | 
			
		||||
		b.control = &pipes{args: args}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.sync = conf.Sync()
 | 
			
		||||
	b.argF = argF
 | 
			
		||||
	b.name = name
 | 
			
		||||
	if wt != nil {
 | 
			
		||||
		b.cp = &pipes{args: wt}
 | 
			
		||||
		b.controlPt = &pipes{args: wt}
 | 
			
		||||
	}
 | 
			
		||||
	b.Cmd = execCommand(BubblewrapName)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	Tmpfs = iota
 | 
			
		||||
	Dir
 | 
			
		||||
	Symlink
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var awkwardArgs = [...]string{
 | 
			
		||||
	Tmpfs:   "--tmpfs",
 | 
			
		||||
	Dir:     "--dir",
 | 
			
		||||
	Symlink: "--symlink",
 | 
			
		||||
}
 | 
			
		||||
@ -1,81 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	UnshareAll = 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, boolArgs[i]...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import "strconv"
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	UID = iota
 | 
			
		||||
	GID
 | 
			
		||||
	Perms
 | 
			
		||||
	Size
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var intArgs = [...]string{
 | 
			
		||||
	UID:   "--uid",
 | 
			
		||||
	GID:   "--gid",
 | 
			
		||||
	Perms: "--perms",
 | 
			
		||||
	Size:  "--size",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Config) intArgs() Builder {
 | 
			
		||||
	// Arg types:
 | 
			
		||||
	//   Perms
 | 
			
		||||
	// are handled by the sequential 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, intArgs[i], strconv.Itoa(*v))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,73 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SetEnv = iota
 | 
			
		||||
 | 
			
		||||
	Bind
 | 
			
		||||
	BindTry
 | 
			
		||||
	DevBind
 | 
			
		||||
	DevBindTry
 | 
			
		||||
	ROBind
 | 
			
		||||
	ROBindTry
 | 
			
		||||
 | 
			
		||||
	Chmod
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var pairArgs = [...]string{
 | 
			
		||||
	SetEnv: "--setenv",
 | 
			
		||||
 | 
			
		||||
	Bind:       "--bind",
 | 
			
		||||
	BindTry:    "--bind-try",
 | 
			
		||||
	DevBind:    "--dev-bind",
 | 
			
		||||
	DevBindTry: "--dev-bind-try",
 | 
			
		||||
	ROBind:     "--ro-bind",
 | 
			
		||||
	ROBindTry:  "--ro-bind-try",
 | 
			
		||||
 | 
			
		||||
	Chmod: "--chmod",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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]}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Arg types:
 | 
			
		||||
	//   Bind
 | 
			
		||||
	//   BindTry
 | 
			
		||||
	//   DevBind
 | 
			
		||||
	//   DevBindTry
 | 
			
		||||
	//   ROBind
 | 
			
		||||
	//   ROBindTry
 | 
			
		||||
	//   Chmod
 | 
			
		||||
	// are handled by the sequential builder
 | 
			
		||||
 | 
			
		||||
	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, pairArgs[i], v[0], v[1])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,65 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	Hostname = iota
 | 
			
		||||
	Chdir
 | 
			
		||||
	UnsetEnv
 | 
			
		||||
	LockFile
 | 
			
		||||
 | 
			
		||||
	RemountRO
 | 
			
		||||
	Procfs
 | 
			
		||||
	DevTmpfs
 | 
			
		||||
	Mqueue
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var stringArgs = [...]string{
 | 
			
		||||
	Hostname: "--hostname",
 | 
			
		||||
	Chdir:    "--chdir",
 | 
			
		||||
	UnsetEnv: "--unsetenv",
 | 
			
		||||
	LockFile: "--lock-file",
 | 
			
		||||
 | 
			
		||||
	RemountRO: "--remount-ro",
 | 
			
		||||
	Procfs:    "--proc",
 | 
			
		||||
	DevTmpfs:  "--dev",
 | 
			
		||||
	Mqueue:    "--mqueue",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Arg types:
 | 
			
		||||
	// 	 RemountRO
 | 
			
		||||
	//   Procfs
 | 
			
		||||
	//   DevTmpfs
 | 
			
		||||
	//   Mqueue
 | 
			
		||||
	// are handled by the sequential builder
 | 
			
		||||
 | 
			
		||||
	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, stringArgs[i], v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -39,46 +39,60 @@ func (c *Config) Bind(src, dest string, opts ...bool) *Config {
 | 
			
		||||
 | 
			
		||||
	if dev {
 | 
			
		||||
		if try {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBindTry], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.Unwrap(), src, dest})
 | 
			
		||||
		} else {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBind], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBind.Unwrap(), src, dest})
 | 
			
		||||
		}
 | 
			
		||||
		return c
 | 
			
		||||
	} else if write {
 | 
			
		||||
		if try {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[BindTry], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{BindTry.Unwrap(), src, dest})
 | 
			
		||||
		} else {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[Bind], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{Bind.Unwrap(), src, dest})
 | 
			
		||||
		}
 | 
			
		||||
		return c
 | 
			
		||||
	} else {
 | 
			
		||||
		if try {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBindTry], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.Unwrap(), src, dest})
 | 
			
		||||
		} else {
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBind], src, dest})
 | 
			
		||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBind.Unwrap(), src, dest})
 | 
			
		||||
		}
 | 
			
		||||
		return c
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dir create dir in sandbox
 | 
			
		||||
// (--dir DEST)
 | 
			
		||||
func (c *Config) Dir(dest string) *Config {
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{Dir.Unwrap(), 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{stringArgs[RemountRO], dest})
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{RemountRO.Unwrap(), dest})
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Procfs mount new procfs in sandbox
 | 
			
		||||
// (--proc DEST)
 | 
			
		||||
func (c *Config) Procfs(dest string) *Config {
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Procfs], dest})
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{Procfs.Unwrap(), dest})
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DevTmpfs mount new dev in sandbox
 | 
			
		||||
// (--dev DEST)
 | 
			
		||||
func (c *Config) DevTmpfs(dest string) *Config {
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{stringArgs[DevTmpfs], dest})
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.Unwrap(), 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.Unwrap(), dest})
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -96,17 +110,28 @@ func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mqueue mount new mqueue in sandbox
 | 
			
		||||
// (--mqueue DEST)
 | 
			
		||||
func (c *Config) Mqueue(dest string) *Config {
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Mqueue], dest})
 | 
			
		||||
// 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dir create dir in sandbox
 | 
			
		||||
// (--dir DEST)
 | 
			
		||||
func (c *Config) Dir(dest string) *Config {
 | 
			
		||||
	c.Filesystem = append(c.Filesystem, &stringF{awkwardArgs[Dir], dest})
 | 
			
		||||
// 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,9 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(new(PermConfig[SymlinkConfig]))
 | 
			
		||||
	gob.Register(new(PermConfig[*TmpfsConfig]))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	// unshare every namespace we support by default if nil
 | 
			
		||||
	// (--unshare-all)
 | 
			
		||||
@ -52,7 +45,7 @@ type Config struct {
 | 
			
		||||
	LockFile []string `json:"lock_file,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// ordered filesystem args
 | 
			
		||||
	Filesystem []FSBuilder
 | 
			
		||||
	Filesystem []FSBuilder `json:"filesystem,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// change permissions (must already exist)
 | 
			
		||||
	// (--chmod OCTAL PATH)
 | 
			
		||||
@ -78,6 +71,8 @@ type Config struct {
 | 
			
		||||
	    --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
 | 
			
		||||
	    --file FD DEST               Copy from FD to destination DEST
 | 
			
		||||
@ -121,85 +116,3 @@ type UnshareConfig struct {
 | 
			
		||||
	// create new cgroup namespace
 | 
			
		||||
	CGroup bool `json:"cgroup"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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, intArgs[Perms], 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, intArgs[Size], strconv.Itoa(t.Size))
 | 
			
		||||
	}
 | 
			
		||||
	*args = append(*args, awkwardArgs[Tmpfs], t.Dir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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, awkwardArgs[Symlink], 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, pairArgs[Chmod], strconv.FormatInt(int64(mode), 8), path)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,176 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
package bwrap_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestConfig_Args(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		conf *Config
 | 
			
		||||
		conf *bwrap.Config
 | 
			
		||||
		want []string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "bind",
 | 
			
		||||
			conf: (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),
 | 
			
		||||
			want: []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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "dir remount-ro proc dev mqueue",
 | 
			
		||||
			conf: (new(bwrap.Config)).
 | 
			
		||||
				Dir("/.fortify").
 | 
			
		||||
				RemountRO("/home").
 | 
			
		||||
				Procfs("/proc").
 | 
			
		||||
				DevTmpfs("/dev").
 | 
			
		||||
				Mqueue("/dev/mqueue"),
 | 
			
		||||
			want: []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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "tmpfs",
 | 
			
		||||
			conf: (new(bwrap.Config)).
 | 
			
		||||
				Tmpfs("/run/user", 8192).
 | 
			
		||||
				Tmpfs("/run/dbus", 8192, 0755),
 | 
			
		||||
			want: []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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "symlink",
 | 
			
		||||
			conf: (new(bwrap.Config)).
 | 
			
		||||
				Symlink("/.fortify/sbin/init", "/sbin/init").
 | 
			
		||||
				Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
 | 
			
		||||
			want: []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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "overlayfs",
 | 
			
		||||
			conf: (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"),
 | 
			
		||||
			want: []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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "unshare",
 | 
			
		||||
			conf: &bwrap.Config{Unshare: &bwrap.UnshareConfig{
 | 
			
		||||
				User:   false,
 | 
			
		||||
				IPC:    false,
 | 
			
		||||
				PID:    false,
 | 
			
		||||
				Net:    false,
 | 
			
		||||
				UTS:    false,
 | 
			
		||||
				CGroup: false,
 | 
			
		||||
			}},
 | 
			
		||||
			want: []string{"--disable-userns", "--assert-userns-disabled"},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "uid gid sync",
 | 
			
		||||
			conf: (new(bwrap.Config)).
 | 
			
		||||
				SetUID(1971).
 | 
			
		||||
				SetGID(100).
 | 
			
		||||
				SetSync(os.Stdin),
 | 
			
		||||
			want: []string{
 | 
			
		||||
				"--unshare-all", "--unshare-user",
 | 
			
		||||
				"--disable-userns", "--assert-userns-disabled",
 | 
			
		||||
				// SetUID(1971)
 | 
			
		||||
				"--uid", "1971",
 | 
			
		||||
				// SetGID(100)
 | 
			
		||||
				"--gid", "100",
 | 
			
		||||
				// SetSync(os.Stdin)
 | 
			
		||||
				// this is set when the process is created
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "hostname chdir setenv unsetenv lockfile chmod",
 | 
			
		||||
			conf: &bwrap.Config{
 | 
			
		||||
				Hostname: "fortify",
 | 
			
		||||
				Chdir:    "/.fortify",
 | 
			
		||||
				SetEnv:   map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
 | 
			
		||||
				UnsetEnv: []string{"HOME", "HOST"},
 | 
			
		||||
				LockFile: []string{"/.fortify/lock"},
 | 
			
		||||
				Chmod:    map[string]os.FileMode{"/.fortify/sbin/init": 0755},
 | 
			
		||||
			},
 | 
			
		||||
			want: []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",
 | 
			
		||||
				// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
 | 
			
		||||
				"--chmod", "755", "/.fortify/sbin/init",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			name: "xdg-dbus-proxy constraint sample",
 | 
			
		||||
			conf: (&Config{
 | 
			
		||||
			conf: (&bwrap.Config{
 | 
			
		||||
				Unshare:       nil,
 | 
			
		||||
				UserNS:        false,
 | 
			
		||||
				Clearenv:      true,
 | 
			
		||||
@ -69,148 +226,6 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
				"--ro-bind", "/etc", "/etc",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "fortify permissive default nixos",
 | 
			
		||||
			conf: (&Config{
 | 
			
		||||
				Unshare:  nil,
 | 
			
		||||
				Net:      true,
 | 
			
		||||
				UserNS:   true,
 | 
			
		||||
				Clearenv: true,
 | 
			
		||||
				SetEnv: map[string]string{
 | 
			
		||||
					"HOME":              "/home/chronos",
 | 
			
		||||
					"TERM":              "xterm-256color",
 | 
			
		||||
					"FORTIFY_INIT":      "3",
 | 
			
		||||
					"XDG_RUNTIME_DIR":   "/run/user/150",
 | 
			
		||||
					"XDG_SESSION_CLASS": "user",
 | 
			
		||||
					"XDG_SESSION_TYPE":  "tty",
 | 
			
		||||
					"SHELL":             "/run/current-system/sw/bin/zsh",
 | 
			
		||||
					"USER":              "chronos",
 | 
			
		||||
				},
 | 
			
		||||
				DieWithParent: true,
 | 
			
		||||
				AsInit:        true,
 | 
			
		||||
			}).SetUID(65534).SetGID(65534).
 | 
			
		||||
				Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
				Bind("/bin", "/bin", false, true).
 | 
			
		||||
				Bind("/boot", "/boot", false, true).
 | 
			
		||||
				Bind("/etc", "/etc", false, true).
 | 
			
		||||
				Bind("/home", "/home", false, true).
 | 
			
		||||
				Bind("/lib", "/lib", false, true).
 | 
			
		||||
				Bind("/lib64", "/lib64", false, true).
 | 
			
		||||
				Bind("/nix", "/nix", false, true).
 | 
			
		||||
				Bind("/root", "/root", false, true).
 | 
			
		||||
				Bind("/srv", "/srv", false, true).
 | 
			
		||||
				Bind("/sys", "/sys", false, true).
 | 
			
		||||
				Bind("/usr", "/usr", false, true).
 | 
			
		||||
				Bind("/var", "/var", false, true).
 | 
			
		||||
				Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
 | 
			
		||||
				Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
 | 
			
		||||
				Bind("/run/binfmt", "/run/binfmt", false, true).
 | 
			
		||||
				Bind("/run/booted-system", "/run/booted-system", false, true).
 | 
			
		||||
				Bind("/run/credentials", "/run/credentials", false, true).
 | 
			
		||||
				Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
 | 
			
		||||
				Bind("/run/current-system", "/run/current-system", false, true).
 | 
			
		||||
				Bind("/run/host", "/run/host", false, true).
 | 
			
		||||
				Bind("/run/keys", "/run/keys", false, true).
 | 
			
		||||
				Bind("/run/libvirt", "/run/libvirt", false, true).
 | 
			
		||||
				Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
 | 
			
		||||
				Bind("/run/lock", "/run/lock", false, true).
 | 
			
		||||
				Bind("/run/log", "/run/log", false, true).
 | 
			
		||||
				Bind("/run/lvm", "/run/lvm", false, true).
 | 
			
		||||
				Bind("/run/mount", "/run/mount", false, true).
 | 
			
		||||
				Bind("/run/nginx", "/run/nginx", false, true).
 | 
			
		||||
				Bind("/run/nscd", "/run/nscd", false, true).
 | 
			
		||||
				Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
 | 
			
		||||
				Bind("/run/pppd", "/run/pppd", false, true).
 | 
			
		||||
				Bind("/run/resolvconf", "/run/resolvconf", false, true).
 | 
			
		||||
				Bind("/run/sddm", "/run/sddm", false, true).
 | 
			
		||||
				Bind("/run/syncoid", "/run/syncoid", false, true).
 | 
			
		||||
				Bind("/run/systemd", "/run/systemd", false, true).
 | 
			
		||||
				Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
 | 
			
		||||
				Bind("/run/udev", "/run/udev", false, true).
 | 
			
		||||
				Bind("/run/udisks2", "/run/udisks2", false, true).
 | 
			
		||||
				Bind("/run/utmp", "/run/utmp", false, true).
 | 
			
		||||
				Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
 | 
			
		||||
				Bind("/run/wrappers", "/run/wrappers", false, true).
 | 
			
		||||
				Bind("/run/zed.pid", "/run/zed.pid", false, true).
 | 
			
		||||
				Bind("/run/zed.state", "/run/zed.state", false, true).
 | 
			
		||||
				Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
 | 
			
		||||
				Tmpfs("/tmp/fortify.1971", 1048576).
 | 
			
		||||
				Tmpfs("/run/user", 1048576).
 | 
			
		||||
				Tmpfs("/run/user/150", 8388608).
 | 
			
		||||
				Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd").
 | 
			
		||||
				Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group").
 | 
			
		||||
				Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd").
 | 
			
		||||
				Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group").
 | 
			
		||||
				Tmpfs("/var/run/nscd", 8192),
 | 
			
		||||
			want: []string{
 | 
			
		||||
				"--unshare-all", "--unshare-user", "--share-net",
 | 
			
		||||
				"--clearenv", "--die-with-parent", "--as-pid-1",
 | 
			
		||||
				"--uid", "65534",
 | 
			
		||||
				"--gid", "65534",
 | 
			
		||||
				"--setenv", "FORTIFY_INIT", "3",
 | 
			
		||||
				"--setenv", "HOME", "/home/chronos",
 | 
			
		||||
				"--setenv", "SHELL", "/run/current-system/sw/bin/zsh",
 | 
			
		||||
				"--setenv", "TERM", "xterm-256color",
 | 
			
		||||
				"--setenv", "USER", "chronos",
 | 
			
		||||
				"--setenv", "XDG_RUNTIME_DIR", "/run/user/150",
 | 
			
		||||
				"--setenv", "XDG_SESSION_CLASS", "user",
 | 
			
		||||
				"--setenv", "XDG_SESSION_TYPE", "tty",
 | 
			
		||||
				"--proc", "/proc", "--dev", "/dev",
 | 
			
		||||
				"--mqueue", "/dev/mqueue",
 | 
			
		||||
				"--bind", "/bin", "/bin",
 | 
			
		||||
				"--bind", "/boot", "/boot",
 | 
			
		||||
				"--bind", "/etc", "/etc",
 | 
			
		||||
				"--bind", "/home", "/home",
 | 
			
		||||
				"--bind", "/lib", "/lib",
 | 
			
		||||
				"--bind", "/lib64", "/lib64",
 | 
			
		||||
				"--bind", "/nix", "/nix",
 | 
			
		||||
				"--bind", "/root", "/root",
 | 
			
		||||
				"--bind", "/srv", "/srv",
 | 
			
		||||
				"--bind", "/sys", "/sys",
 | 
			
		||||
				"--bind", "/usr", "/usr",
 | 
			
		||||
				"--bind", "/var", "/var",
 | 
			
		||||
				"--bind", "/run/NetworkManager", "/run/NetworkManager",
 | 
			
		||||
				"--bind", "/run/agetty.reload", "/run/agetty.reload",
 | 
			
		||||
				"--bind", "/run/binfmt", "/run/binfmt",
 | 
			
		||||
				"--bind", "/run/booted-system", "/run/booted-system",
 | 
			
		||||
				"--bind", "/run/credentials", "/run/credentials",
 | 
			
		||||
				"--bind", "/run/cryptsetup", "/run/cryptsetup",
 | 
			
		||||
				"--bind", "/run/current-system", "/run/current-system",
 | 
			
		||||
				"--bind", "/run/host", "/run/host",
 | 
			
		||||
				"--bind", "/run/keys", "/run/keys",
 | 
			
		||||
				"--bind", "/run/libvirt", "/run/libvirt",
 | 
			
		||||
				"--bind", "/run/libvirtd.pid", "/run/libvirtd.pid",
 | 
			
		||||
				"--bind", "/run/lock", "/run/lock",
 | 
			
		||||
				"--bind", "/run/log", "/run/log",
 | 
			
		||||
				"--bind", "/run/lvm", "/run/lvm",
 | 
			
		||||
				"--bind", "/run/mount", "/run/mount",
 | 
			
		||||
				"--bind", "/run/nginx", "/run/nginx",
 | 
			
		||||
				"--bind", "/run/nscd", "/run/nscd",
 | 
			
		||||
				"--bind", "/run/opengl-driver", "/run/opengl-driver",
 | 
			
		||||
				"--bind", "/run/pppd", "/run/pppd",
 | 
			
		||||
				"--bind", "/run/resolvconf", "/run/resolvconf",
 | 
			
		||||
				"--bind", "/run/sddm", "/run/sddm",
 | 
			
		||||
				"--bind", "/run/syncoid", "/run/syncoid",
 | 
			
		||||
				"--bind", "/run/systemd", "/run/systemd",
 | 
			
		||||
				"--bind", "/run/tmpfiles.d", "/run/tmpfiles.d",
 | 
			
		||||
				"--bind", "/run/udev", "/run/udev",
 | 
			
		||||
				"--bind", "/run/udisks2", "/run/udisks2",
 | 
			
		||||
				"--bind", "/run/utmp", "/run/utmp",
 | 
			
		||||
				"--bind", "/run/virtlogd.pid", "/run/virtlogd.pid",
 | 
			
		||||
				"--bind", "/run/wrappers", "/run/wrappers",
 | 
			
		||||
				"--bind", "/run/zed.pid", "/run/zed.pid",
 | 
			
		||||
				"--bind", "/run/zed.state", "/run/zed.state",
 | 
			
		||||
				"--bind", "/tmp/fortify.1971/tmpdir/150", "/tmp",
 | 
			
		||||
				"--size", "1048576", "--tmpfs", "/tmp/fortify.1971",
 | 
			
		||||
				"--size", "1048576", "--tmpfs", "/run/user",
 | 
			
		||||
				"--size", "8388608", "--tmpfs", "/run/user/150",
 | 
			
		||||
				"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd",
 | 
			
		||||
				"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group",
 | 
			
		||||
				"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd",
 | 
			
		||||
				"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group",
 | 
			
		||||
				"--size", "8192", "--tmpfs", "/var/run/nscd",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
@ -220,4 +235,21 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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", "", "")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("sync file", func(t *testing.T) {
 | 
			
		||||
		if s := (new(bwrap.Config)).SetSync(os.Stdout).Sync(); s != os.Stdout {
 | 
			
		||||
			t.Errorf("Sync() = %v", s)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										223
									
								
								helper/bwrap/sequential.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								helper/bwrap/sequential.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,223 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(new(PermConfig[SymlinkConfig]))
 | 
			
		||||
	gob.Register(new(PermConfig[*TmpfsConfig]))
 | 
			
		||||
	gob.Register(new(OverlayConfig))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PositionalArg int
 | 
			
		||||
 | 
			
		||||
func (p PositionalArg) Unwrap() 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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.Unwrap(), 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.Unwrap(), strconv.Itoa(t.Size))
 | 
			
		||||
	}
 | 
			
		||||
	*args = append(*args, Tmpfs.Unwrap(), 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.Unwrap(), src)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if o.Persist != nil {
 | 
			
		||||
		if o.Persist[0] != "" && o.Persist[1] != "" {
 | 
			
		||||
			// --overlay RWSRC WORKDIR
 | 
			
		||||
			*args = append(*args, Overlay.Unwrap(), o.Persist[0], o.Persist[1])
 | 
			
		||||
		} else {
 | 
			
		||||
			// --ro-overlay
 | 
			
		||||
			*args = append(*args, ROOverlay.Unwrap())
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// --tmp-overlay
 | 
			
		||||
		*args = append(*args, TmpOverlay.Unwrap())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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.Unwrap(), 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.Unwrap(), strconv.FormatInt(int64(mode), 8), path)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										249
									
								
								helper/bwrap/static.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								helper/bwrap/static.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,249 @@
 | 
			
		||||
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])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -62,45 +62,14 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			Bind("/lib64", "/lib64", false, true).
 | 
			
		||||
			Bind("/nix", "/nix", false, true).
 | 
			
		||||
			Bind("/root", "/root", false, true).
 | 
			
		||||
			Bind("/run", "/run", false, true).
 | 
			
		||||
			Bind("/srv", "/srv", false, true).
 | 
			
		||||
			Bind("/sys", "/sys", false, true).
 | 
			
		||||
			Bind("/usr", "/usr", false, true).
 | 
			
		||||
			Bind("/var", "/var", false, true).
 | 
			
		||||
			Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
 | 
			
		||||
			Bind("/run/binfmt", "/run/binfmt", false, true).
 | 
			
		||||
			Bind("/run/booted-system", "/run/booted-system", false, true).
 | 
			
		||||
			Bind("/run/credentials", "/run/credentials", false, true).
 | 
			
		||||
			Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
 | 
			
		||||
			Bind("/run/current-system", "/run/current-system", false, true).
 | 
			
		||||
			Bind("/run/host", "/run/host", false, true).
 | 
			
		||||
			Bind("/run/keys", "/run/keys", false, true).
 | 
			
		||||
			Bind("/run/libvirt", "/run/libvirt", false, true).
 | 
			
		||||
			Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
 | 
			
		||||
			Bind("/run/lock", "/run/lock", false, true).
 | 
			
		||||
			Bind("/run/log", "/run/log", false, true).
 | 
			
		||||
			Bind("/run/lvm", "/run/lvm", false, true).
 | 
			
		||||
			Bind("/run/mount", "/run/mount", false, true).
 | 
			
		||||
			Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
 | 
			
		||||
			Bind("/run/nginx", "/run/nginx", false, true).
 | 
			
		||||
			Bind("/run/nixos", "/run/nixos", false, true).
 | 
			
		||||
			Bind("/run/nscd", "/run/nscd", false, true).
 | 
			
		||||
			Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
 | 
			
		||||
			Bind("/run/pppd", "/run/pppd", false, true).
 | 
			
		||||
			Bind("/run/resolvconf", "/run/resolvconf", false, true).
 | 
			
		||||
			Bind("/run/sddm", "/run/sddm", false, true).
 | 
			
		||||
			Bind("/run/store", "/run/store", false, true).
 | 
			
		||||
			Bind("/run/syncoid", "/run/syncoid", false, true).
 | 
			
		||||
			Bind("/run/system", "/run/system", false, true).
 | 
			
		||||
			Bind("/run/systemd", "/run/systemd", false, true).
 | 
			
		||||
			Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
 | 
			
		||||
			Bind("/run/udev", "/run/udev", false, true).
 | 
			
		||||
			Bind("/run/udisks2", "/run/udisks2", false, true).
 | 
			
		||||
			Bind("/run/utmp", "/run/utmp", false, true).
 | 
			
		||||
			Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
 | 
			
		||||
			Bind("/run/wrappers", "/run/wrappers", false, true).
 | 
			
		||||
			Bind("/run/zed.pid", "/run/zed.pid", false, true).
 | 
			
		||||
			Bind("/run/zed.state", "/run/zed.state", false, true).
 | 
			
		||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
			
		||||
			Tmpfs("/run/user/1971", 8192).
 | 
			
		||||
			Tmpfs("/run/dbus", 8192).
 | 
			
		||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
@ -317,46 +286,15 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			Bind("/lib64", "/lib64", false, true).
 | 
			
		||||
			Bind("/nix", "/nix", false, true).
 | 
			
		||||
			Bind("/root", "/root", false, true).
 | 
			
		||||
			Bind("/run", "/run", false, true).
 | 
			
		||||
			Bind("/srv", "/srv", false, true).
 | 
			
		||||
			Bind("/sys", "/sys", false, true).
 | 
			
		||||
			Bind("/usr", "/usr", false, true).
 | 
			
		||||
			Bind("/var", "/var", false, true).
 | 
			
		||||
			Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
 | 
			
		||||
			Bind("/run/binfmt", "/run/binfmt", false, true).
 | 
			
		||||
			Bind("/run/booted-system", "/run/booted-system", false, true).
 | 
			
		||||
			Bind("/run/credentials", "/run/credentials", false, true).
 | 
			
		||||
			Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
 | 
			
		||||
			Bind("/run/current-system", "/run/current-system", false, true).
 | 
			
		||||
			Bind("/run/host", "/run/host", false, true).
 | 
			
		||||
			Bind("/run/keys", "/run/keys", false, true).
 | 
			
		||||
			Bind("/run/libvirt", "/run/libvirt", false, true).
 | 
			
		||||
			Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
 | 
			
		||||
			Bind("/run/lock", "/run/lock", false, true).
 | 
			
		||||
			Bind("/run/log", "/run/log", false, true).
 | 
			
		||||
			Bind("/run/lvm", "/run/lvm", false, true).
 | 
			
		||||
			Bind("/run/mount", "/run/mount", false, true).
 | 
			
		||||
			Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
 | 
			
		||||
			Bind("/run/nginx", "/run/nginx", false, true).
 | 
			
		||||
			Bind("/run/nixos", "/run/nixos", false, true).
 | 
			
		||||
			Bind("/run/nscd", "/run/nscd", false, true).
 | 
			
		||||
			Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
 | 
			
		||||
			Bind("/run/pppd", "/run/pppd", false, true).
 | 
			
		||||
			Bind("/run/resolvconf", "/run/resolvconf", false, true).
 | 
			
		||||
			Bind("/run/sddm", "/run/sddm", false, true).
 | 
			
		||||
			Bind("/run/store", "/run/store", false, true).
 | 
			
		||||
			Bind("/run/syncoid", "/run/syncoid", false, true).
 | 
			
		||||
			Bind("/run/system", "/run/system", false, true).
 | 
			
		||||
			Bind("/run/systemd", "/run/systemd", false, true).
 | 
			
		||||
			Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
 | 
			
		||||
			Bind("/run/udev", "/run/udev", false, true).
 | 
			
		||||
			Bind("/run/udisks2", "/run/udisks2", false, true).
 | 
			
		||||
			Bind("/run/utmp", "/run/utmp", false, true).
 | 
			
		||||
			Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
 | 
			
		||||
			Bind("/run/wrappers", "/run/wrappers", false, true).
 | 
			
		||||
			Bind("/run/zed.pid", "/run/zed.pid", false, true).
 | 
			
		||||
			Bind("/run/zed.state", "/run/zed.state", false, true).
 | 
			
		||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
			
		||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
			
		||||
			Tmpfs("/run/user/1971", 8192).
 | 
			
		||||
			Tmpfs("/run/dbus", 8192).
 | 
			
		||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ package app_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@ -128,12 +127,12 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubNixOS) Exit(code int) {
 | 
			
		||||
	panic("called exit on stub with code " + strconv.Itoa(code))
 | 
			
		||||
func (s *stubNixOS) EvalSymlinks(path string) (string, error) {
 | 
			
		||||
	return path, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubNixOS) Stdout() io.Writer {
 | 
			
		||||
	panic("requested stdout")
 | 
			
		||||
func (s *stubNixOS) Exit(code int) {
 | 
			
		||||
	panic("called exit on stub with code " + strconv.Itoa(code))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubNixOS) Paths() linux.Paths {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package app_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
@ -48,14 +49,22 @@ func TestApp(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
			t.Run("compare bwrap", func(t *testing.T) {
 | 
			
		||||
				if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
 | 
			
		||||
					t.Errorf("seal: bwrap = %#v, want %#v",
 | 
			
		||||
						gotBwrap, tc.wantBwrap)
 | 
			
		||||
					t.Errorf("seal: bwrap =\n%s\n, want\n%s",
 | 
			
		||||
						mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mustMarshal(v any) string {
 | 
			
		||||
	if b, err := json.Marshal(v); err != nil {
 | 
			
		||||
		panic(err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
		return string(b)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
 | 
			
		||||
	e = make([]fs.DirEntry, len(names))
 | 
			
		||||
	for i, name := range names {
 | 
			
		||||
 | 
			
		||||
@ -194,7 +194,6 @@ func (a *app) Seal(config *fst.Config) error {
 | 
			
		||||
				switch p {
 | 
			
		||||
				case "/proc":
 | 
			
		||||
				case "/dev":
 | 
			
		||||
				case "/run":
 | 
			
		||||
				case "/tmp":
 | 
			
		||||
				case "/mnt":
 | 
			
		||||
				case "/etc":
 | 
			
		||||
@ -205,23 +204,7 @@ func (a *app) Seal(config *fst.Config) error {
 | 
			
		||||
			}
 | 
			
		||||
			conf.Filesystem = append(conf.Filesystem, b...)
 | 
			
		||||
		}
 | 
			
		||||
		// bind entries in /run
 | 
			
		||||
		if d, err := a.os.ReadDir("/run"); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			b := make([]*fst.FilesystemConfig, 0, len(d))
 | 
			
		||||
			for _, ent := range d {
 | 
			
		||||
				name := ent.Name()
 | 
			
		||||
				switch name {
 | 
			
		||||
				case "user":
 | 
			
		||||
				case "dbus":
 | 
			
		||||
				default:
 | 
			
		||||
					p := "/run/" + name
 | 
			
		||||
					b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true})
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			conf.Filesystem = append(conf.Filesystem, b...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// hide nscd from sandbox if present
 | 
			
		||||
		nscd := "/var/run/nscd"
 | 
			
		||||
		if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
			
		||||
 | 
			
		||||
@ -170,7 +170,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
 | 
			
		||||
			appID := seal.fid
 | 
			
		||||
			if appID == "" {
 | 
			
		||||
				// use instance ID in case app id is not set
 | 
			
		||||
				appID = "moe.ophivana.fortify." + seal.id
 | 
			
		||||
				appID = "uk.gensokyo.fortify." + seal.id
 | 
			
		||||
			}
 | 
			
		||||
			seal.sys.Wayland(wt, wp, appID, seal.id)
 | 
			
		||||
			seal.sys.bwrap.Bind(wt, w)
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
package linux
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"path"
 | 
			
		||||
@ -30,10 +29,10 @@ type System interface {
 | 
			
		||||
	Stat(name string) (fs.FileInfo, error)
 | 
			
		||||
	// Open provides [os.Open]
 | 
			
		||||
	Open(name string) (fs.File, error)
 | 
			
		||||
	// EvalSymlinks provides [filepath.EvalSymlinks]
 | 
			
		||||
	EvalSymlinks(path string) (string, error)
 | 
			
		||||
	// Exit provides [os.Exit].
 | 
			
		||||
	Exit(code int)
 | 
			
		||||
	// Stdout provides [os.Stdout].
 | 
			
		||||
	Stdout() io.Writer
 | 
			
		||||
 | 
			
		||||
	// Paths returns a populated [Paths] struct.
 | 
			
		||||
	Paths() Paths
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
package linux
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
@ -35,8 +37,8 @@ func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.Lookup
 | 
			
		||||
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) Open(name string) (fs.File, error)            { return os.Open(name) }
 | 
			
		||||
func (s *Std) EvalSymlinks(path string) (string, error)     { return filepath.EvalSymlinks(path) }
 | 
			
		||||
func (s *Std) Exit(code int)                                { fmsg.Exit(code) }
 | 
			
		||||
func (s *Std) Stdout() io.Writer                            { return os.Stdout }
 | 
			
		||||
 | 
			
		||||
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
 | 
			
		||||
 | 
			
		||||
@ -79,9 +81,15 @@ func (s *Std) Uid(aid int) (int, error) {
 | 
			
		||||
		cmd.Stderr = os.Stderr // pass through fatal messages
 | 
			
		||||
		cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
 | 
			
		||||
		cmd.Dir = "/"
 | 
			
		||||
		var p []byte
 | 
			
		||||
		var (
 | 
			
		||||
			p         []byte
 | 
			
		||||
			exitError *exec.ExitError
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		if p, u.err = cmd.Output(); u.err == nil {
 | 
			
		||||
			u.uid, u.err = strconv.Atoi(string(p))
 | 
			
		||||
		} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
 | 
			
		||||
			u.err = syscall.EACCES
 | 
			
		||||
		}
 | 
			
		||||
		return u.uid, u.err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,9 @@ package internal
 | 
			
		||||
import "path"
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Fsu   = compPoison
 | 
			
		||||
	Finit = compPoison
 | 
			
		||||
	Fortify = compPoison
 | 
			
		||||
	Fsu     = compPoison
 | 
			
		||||
	Finit   = compPoison
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Path(p string) (string, bool) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								main.go
									
									
									
									
									
								
							@ -4,6 +4,7 @@ import (
 | 
			
		||||
	_ "embed"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@ -16,7 +17,6 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ func init() {
 | 
			
		||||
	flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var os = new(linux.Std)
 | 
			
		||||
var sys linux.System = new(linux.Std)
 | 
			
		||||
 | 
			
		||||
type gl []string
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ func main() {
 | 
			
		||||
		fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]")
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		fmt.Println("Commands:")
 | 
			
		||||
		w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0)
 | 
			
		||||
		w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
 | 
			
		||||
		commands := [][2]string{
 | 
			
		||||
			{"app", "Launch app defined by the specified config file"},
 | 
			
		||||
			{"run", "Configure and start a permissive default sandbox"},
 | 
			
		||||
@ -128,24 +128,20 @@ func main() {
 | 
			
		||||
		// Ignore errors; set is set for ExitOnError.
 | 
			
		||||
		_ = set.Parse(args[1:])
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			config   *fst.Config
 | 
			
		||||
			instance *state.State
 | 
			
		||||
			name     string
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		if len(set.Args()) != 1 {
 | 
			
		||||
		switch len(set.Args()) {
 | 
			
		||||
		case 0: // system
 | 
			
		||||
			printShowSystem(short)
 | 
			
		||||
		case 1: // instance
 | 
			
		||||
			name := set.Args()[0]
 | 
			
		||||
			config, instance := tryShort(name)
 | 
			
		||||
			if config == nil {
 | 
			
		||||
				config = tryPath(name)
 | 
			
		||||
			}
 | 
			
		||||
			printShowInstance(instance, config, short)
 | 
			
		||||
		default:
 | 
			
		||||
			fmsg.Fatal("show requires 1 argument")
 | 
			
		||||
		} else {
 | 
			
		||||
			name = set.Args()[0]
 | 
			
		||||
			config, instance = tryShort(name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if config == nil {
 | 
			
		||||
			config = tryPath(name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		printShow(instance, config, short)
 | 
			
		||||
		fmsg.Exit(0)
 | 
			
		||||
	case "app": // launch app from configuration file
 | 
			
		||||
		if len(args) < 2 {
 | 
			
		||||
@ -211,7 +207,7 @@ func main() {
 | 
			
		||||
			passwdOnce sync.Once
 | 
			
		||||
			passwdFunc = func() {
 | 
			
		||||
				var us string
 | 
			
		||||
				if uid, err := os.Uid(aid); err != nil {
 | 
			
		||||
				if uid, err := sys.Uid(aid); err != nil {
 | 
			
		||||
					fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
 | 
			
		||||
				} else {
 | 
			
		||||
					us = strconv.Itoa(uid)
 | 
			
		||||
@ -292,7 +288,7 @@ func main() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runApp(config *fst.Config) {
 | 
			
		||||
	a, err := app.New(os)
 | 
			
		||||
	a, err := app.New(sys)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot create app: %s\n", err)
 | 
			
		||||
	} else if err = a.Seal(config); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
buildGoModule rec {
 | 
			
		||||
  pname = "fortify";
 | 
			
		||||
  version = "0.2.8";
 | 
			
		||||
  version = "0.2.9";
 | 
			
		||||
 | 
			
		||||
  src = builtins.path {
 | 
			
		||||
    name = "fortify-src";
 | 
			
		||||
@ -45,6 +45,7 @@ buildGoModule rec {
 | 
			
		||||
        Version = "v${version}";
 | 
			
		||||
        Fsu = "/run/wrappers/bin/fsu";
 | 
			
		||||
        Finit = "${placeholder "out"}/libexec/finit";
 | 
			
		||||
        Fortify = "${placeholder "out"}/bin/fortify";
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  # nix build environment does not allow acls
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								parse.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								parse.go
									
									
									
									
									
								
							@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	direct "os"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
@ -38,7 +38,7 @@ func tryPath(name string) (config *fst.Config) {
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		r = direct.Stdin
 | 
			
		||||
		r = os.Stdin
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.NewDecoder(r).Decode(&config); err != nil {
 | 
			
		||||
@ -61,7 +61,7 @@ func tryFd(name string) io.ReadCloser {
 | 
			
		||||
			}
 | 
			
		||||
			fmsg.Fatalf("cannot get fd %d: %v", fd, errno)
 | 
			
		||||
		}
 | 
			
		||||
		return direct.NewFile(fd, strconv.Itoa(v))
 | 
			
		||||
		return os.NewFile(fd, strconv.Itoa(v))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -85,7 +85,7 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
 | 
			
		||||
	if likePrefix && len(name) >= 8 {
 | 
			
		||||
		fmsg.VPrintln("argument looks like prefix")
 | 
			
		||||
 | 
			
		||||
		s := state.NewMulti(os.Paths().RunDirPath)
 | 
			
		||||
		s := state.NewMulti(sys.Paths().RunDirPath)
 | 
			
		||||
		if entries, err := state.Join(s); err != nil {
 | 
			
		||||
			fmsg.Printf("cannot join store: %v", err)
 | 
			
		||||
			// drop to fetch from file
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								print.go
									
									
									
									
									
								
							@ -16,14 +16,37 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func printShow(instance *state.State, config *fst.Config, short bool) {
 | 
			
		||||
	if flagJSON {
 | 
			
		||||
		v := any(config)
 | 
			
		||||
		if instance != nil {
 | 
			
		||||
			v = instance
 | 
			
		||||
		}
 | 
			
		||||
func printShowSystem(short bool) {
 | 
			
		||||
	info := new(fst.Info)
 | 
			
		||||
 | 
			
		||||
		printJSON(v)
 | 
			
		||||
	// get fid by querying uid of aid 0
 | 
			
		||||
	if uid, err := sys.Uid(0); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		info.User = (uid / 10000) - 100
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if flagJSON {
 | 
			
		||||
		printJSON(info)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(w, "User:\t%d\n", info.User)
 | 
			
		||||
 | 
			
		||||
	if err := w.Flush(); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot flush tabwriter: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printShowInstance(instance *state.State, config *fst.Config, short bool) {
 | 
			
		||||
	if flagJSON {
 | 
			
		||||
		if instance != nil {
 | 
			
		||||
			printJSON(instance)
 | 
			
		||||
		} else {
 | 
			
		||||
			printJSON(config)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -171,7 +194,7 @@ func printPs(short bool) {
 | 
			
		||||
	now := time.Now().UTC()
 | 
			
		||||
 | 
			
		||||
	var entries state.Entries
 | 
			
		||||
	s := state.NewMulti(os.Paths().RunDirPath)
 | 
			
		||||
	s := state.NewMulti(sys.Paths().RunDirPath)
 | 
			
		||||
	if e, err := state.Join(s); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot join store: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								test.nix
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								test.nix
									
									
									
									
									
								
							@ -19,11 +19,19 @@ nixosTest {
 | 
			
		||||
  nodes.machine =
 | 
			
		||||
    { lib, pkgs, ... }:
 | 
			
		||||
    {
 | 
			
		||||
      users.users.alice = {
 | 
			
		||||
        isNormalUser = true;
 | 
			
		||||
        description = "Alice Foobar";
 | 
			
		||||
        password = "foobar";
 | 
			
		||||
        uid = 1000;
 | 
			
		||||
      users.users = {
 | 
			
		||||
        alice = {
 | 
			
		||||
          isNormalUser = true;
 | 
			
		||||
          description = "Alice Foobar";
 | 
			
		||||
          password = "foobar";
 | 
			
		||||
          uid = 1000;
 | 
			
		||||
        };
 | 
			
		||||
        untrusted = {
 | 
			
		||||
          isNormalUser = true;
 | 
			
		||||
          description = "Untrusted user";
 | 
			
		||||
          password = "foobar";
 | 
			
		||||
          uid = 1001;
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      home-manager.users.alice.home.stateVersion = "24.11";
 | 
			
		||||
@ -198,8 +206,11 @@ nixosTest {
 | 
			
		||||
    machine.wait_for_file("/run/user/1000/wayland-1")
 | 
			
		||||
    machine.wait_for_file("/tmp/sway-ipc.sock")
 | 
			
		||||
 | 
			
		||||
    # Create fortify aid 0 home directory:
 | 
			
		||||
    machine.succeed("install -dm 0700 -o 1000000 -g 1000000 /var/lib/fortify/u0/a0")
 | 
			
		||||
    # Deny unmapped uid:
 | 
			
		||||
    print(machine.fail("sudo -u untrusted -i ${self.packages.${system}.fortify}/bin/fortify -v run"))
 | 
			
		||||
 | 
			
		||||
    # Create fortify uid 0 state directory:
 | 
			
		||||
    machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
 | 
			
		||||
 | 
			
		||||
    # Start fortify outside Wayland session:
 | 
			
		||||
    print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								wl/c.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								wl/c.go
									
									
									
									
									
								
							@ -74,7 +74,7 @@ static int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, co
 | 
			
		||||
 | 
			
		||||
  struct wp_security_context_v1 *security_context;
 | 
			
		||||
  security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, sync_fd);
 | 
			
		||||
  wp_security_context_v1_set_sandbox_engine(security_context, "moe.ophivana.fortify");
 | 
			
		||||
  wp_security_context_v1_set_sandbox_engine(security_context, "uk.gensokyo.fortify");
 | 
			
		||||
  wp_security_context_v1_set_app_id(security_context, app_id);
 | 
			
		||||
  wp_security_context_v1_set_instance_id(security_context, instance_id);
 | 
			
		||||
  wp_security_context_v1_commit(security_context);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user