Compare commits
	
		
			No commits in common. "5c4058d5ac7e7944973ca8216c258fc50c194e22" and "24618ab9a1524e8b8986a9bf67667288e642fcf1" have entirely different histories.
		
	
	
		
			5c4058d5ac
			...
			24618ab9a1
		
	
		
@ -4,15 +4,12 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type appInfo struct {
 | 
			
		||||
type bundleInfo struct {
 | 
			
		||||
	Name    string `json:"name"`
 | 
			
		||||
	Version string `json:"version"`
 | 
			
		||||
 | 
			
		||||
@ -23,15 +20,13 @@ type appInfo struct {
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Groups []string `json:"groups,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Devel bool `json:"devel,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Userns bool `json:"userns,omitempty"`
 | 
			
		||||
	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]
 | 
			
		||||
	Tty bool `json:"tty,omitempty"`
 | 
			
		||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
@ -43,9 +38,11 @@ type appInfo struct {
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Enablements system.Enablements `json:"enablements"`
 | 
			
		||||
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	// passed through inverted to [bwrap.SyscallPolicy]
 | 
			
		||||
	Devel bool `json:"devel,omitempty"`
 | 
			
		||||
	// passed through to [bwrap.SyscallPolicy]
 | 
			
		||||
	Multiarch bool `json:"multiarch,omitempty"`
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	// passed through to [bwrap.SyscallPolicy]
 | 
			
		||||
	Bluetooth bool `json:"bluetooth,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// allow gpu access within sandbox
 | 
			
		||||
@ -62,64 +59,8 @@ type appInfo struct {
 | 
			
		||||
	ActivationPackage string `json:"activation_package"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
 | 
			
		||||
	config := &fst.Config{
 | 
			
		||||
		ID:   app.ID,
 | 
			
		||||
		Path: argv[0],
 | 
			
		||||
		Args: argv,
 | 
			
		||||
		Confinement: fst.ConfinementConfig{
 | 
			
		||||
			AppID:    app.AppID,
 | 
			
		||||
			Groups:   app.Groups,
 | 
			
		||||
			Username: "fortify",
 | 
			
		||||
			Inner:    path.Join("/data/data", app.ID),
 | 
			
		||||
			Outer:    pathSet.homeDir,
 | 
			
		||||
			Sandbox: &fst.SandboxConfig{
 | 
			
		||||
				Hostname:      formatHostname(app.Name),
 | 
			
		||||
				Devel:         app.Devel,
 | 
			
		||||
				Userns:        app.Userns,
 | 
			
		||||
				Net:           app.Net,
 | 
			
		||||
				Dev:           app.Dev,
 | 
			
		||||
				Tty:           app.Tty || flagDropShell,
 | 
			
		||||
				MapRealUID:    app.MapRealUID,
 | 
			
		||||
				DirectWayland: app.DirectWayland,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
			
		||||
					{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
			
		||||
					{Src: "/etc/resolv.conf"},
 | 
			
		||||
					{Src: "/sys/block"},
 | 
			
		||||
					{Src: "/sys/bus"},
 | 
			
		||||
					{Src: "/sys/class"},
 | 
			
		||||
					{Src: "/sys/dev"},
 | 
			
		||||
					{Src: "/sys/devices"},
 | 
			
		||||
				},
 | 
			
		||||
				Link: [][2]string{
 | 
			
		||||
					{app.CurrentSystem, "/run/current-system"},
 | 
			
		||||
					{"/run/current-system/sw/bin", "/bin"},
 | 
			
		||||
					{"/run/current-system/sw/bin", "/usr/bin"},
 | 
			
		||||
				},
 | 
			
		||||
				Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
			
		||||
				AutoEtc: true,
 | 
			
		||||
			},
 | 
			
		||||
			ExtraPerms: []*fst.ExtraPermConfig{
 | 
			
		||||
				{Path: dataHome, Execute: true},
 | 
			
		||||
				{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
			
		||||
			},
 | 
			
		||||
			SystemBus:   app.SystemBus,
 | 
			
		||||
			SessionBus:  app.SessionBus,
 | 
			
		||||
			Enablements: app.Enablements,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	if app.Multiarch {
 | 
			
		||||
		config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
 | 
			
		||||
	}
 | 
			
		||||
	if app.Bluetooth {
 | 
			
		||||
		config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
 | 
			
		||||
	}
 | 
			
		||||
	return config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadAppInfo(name string, beforeFail func()) *appInfo {
 | 
			
		||||
	bundle := new(appInfo)
 | 
			
		||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
 | 
			
		||||
	bundle := new(bundleInfo)
 | 
			
		||||
	if f, err := os.Open(name); err != nil {
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		log.Fatalf("cannot open bundle: %v", err)
 | 
			
		||||
@ -12,7 +12,9 @@ import (
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/command"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/init0"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
			
		||||
@ -37,6 +39,7 @@ func init() {
 | 
			
		||||
func main() {
 | 
			
		||||
	// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
			
		||||
	sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
			
		||||
	init0.TryArgv0()
 | 
			
		||||
 | 
			
		||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
			
		||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
			
		||||
@ -62,7 +65,9 @@ func main() {
 | 
			
		||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
			
		||||
		Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
 | 
			
		||||
 | 
			
		||||
	// internal commands
 | 
			
		||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
			
		||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		var (
 | 
			
		||||
@ -119,7 +124,7 @@ func main() {
 | 
			
		||||
				Parse bundle and app metadata, do pre-install checks.
 | 
			
		||||
			*/
 | 
			
		||||
 | 
			
		||||
			bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
			
		||||
			bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
			
		||||
			pathSet := pathSetByApp(bundle.ID)
 | 
			
		||||
 | 
			
		||||
			app := bundle
 | 
			
		||||
@ -135,7 +140,7 @@ func main() {
 | 
			
		||||
				log.Printf("metadata path %q is not a file", pathSet.metaPath)
 | 
			
		||||
				return syscall.EBADMSG
 | 
			
		||||
			} else {
 | 
			
		||||
				app = loadAppInfo(pathSet.metaPath, cleanup)
 | 
			
		||||
				app = loadBundleInfo(pathSet.metaPath, cleanup)
 | 
			
		||||
				if app.ID != bundle.ID {
 | 
			
		||||
					cleanup()
 | 
			
		||||
					log.Printf("app %q claims to have identifier %q",
 | 
			
		||||
@ -268,7 +273,7 @@ func main() {
 | 
			
		||||
 | 
			
		||||
			id := args[0]
 | 
			
		||||
			pathSet := pathSetByApp(id)
 | 
			
		||||
			app := loadAppInfo(pathSet.metaPath, func() {})
 | 
			
		||||
			app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
			
		||||
			if app.ID != id {
 | 
			
		||||
				log.Printf("app %q claims to have identifier %q", id, app.ID)
 | 
			
		||||
				return syscall.EBADE
 | 
			
		||||
@ -317,7 +322,51 @@ func main() {
 | 
			
		||||
			}
 | 
			
		||||
			argv = append(argv, args[1:]...)
 | 
			
		||||
 | 
			
		||||
			config := app.toFst(pathSet, argv, flagDropShell)
 | 
			
		||||
			config := &fst.Config{
 | 
			
		||||
				ID:      app.ID,
 | 
			
		||||
				Command: argv,
 | 
			
		||||
				Confinement: fst.ConfinementConfig{
 | 
			
		||||
					AppID:    app.AppID,
 | 
			
		||||
					Groups:   app.Groups,
 | 
			
		||||
					Username: "fortify",
 | 
			
		||||
					Inner:    path.Join("/data/data", app.ID),
 | 
			
		||||
					Outer:    pathSet.homeDir,
 | 
			
		||||
					Sandbox: &fst.SandboxConfig{
 | 
			
		||||
						Hostname:      formatHostname(app.Name),
 | 
			
		||||
						UserNS:        app.UserNS,
 | 
			
		||||
						Net:           app.Net,
 | 
			
		||||
						Dev:           app.Dev,
 | 
			
		||||
						Syscall:       &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
 | 
			
		||||
						NoNewSession:  app.NoNewSession || flagDropShell,
 | 
			
		||||
						MapRealUID:    app.MapRealUID,
 | 
			
		||||
						DirectWayland: app.DirectWayland,
 | 
			
		||||
						Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
							{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
			
		||||
							{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
			
		||||
							{Src: "/etc/resolv.conf"},
 | 
			
		||||
							{Src: "/sys/block"},
 | 
			
		||||
							{Src: "/sys/bus"},
 | 
			
		||||
							{Src: "/sys/class"},
 | 
			
		||||
							{Src: "/sys/dev"},
 | 
			
		||||
							{Src: "/sys/devices"},
 | 
			
		||||
						},
 | 
			
		||||
						Link: [][2]string{
 | 
			
		||||
							{app.CurrentSystem, "/run/current-system"},
 | 
			
		||||
							{"/run/current-system/sw/bin", "/bin"},
 | 
			
		||||
							{"/run/current-system/sw/bin", "/usr/bin"},
 | 
			
		||||
						},
 | 
			
		||||
						Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
			
		||||
						AutoEtc: true,
 | 
			
		||||
					},
 | 
			
		||||
					ExtraPerms: []*fst.ExtraPermConfig{
 | 
			
		||||
						{Path: dataHome, Execute: true},
 | 
			
		||||
						{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
			
		||||
					},
 | 
			
		||||
					SystemBus:   app.SystemBus,
 | 
			
		||||
					SessionBus:  app.SessionBus,
 | 
			
		||||
					Enablements: app.Enablements,
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/*
 | 
			
		||||
				Expose GPU devices.
 | 
			
		||||
 | 
			
		||||
@ -11,14 +11,14 @@ import (
 | 
			
		||||
 | 
			
		||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
			
		||||
	rs := new(fst.RunState)
 | 
			
		||||
	a := app.MustNew(ctx, std)
 | 
			
		||||
	a := app.MustNew(std)
 | 
			
		||||
 | 
			
		||||
	if sa, err := a.Seal(config); err != nil {
 | 
			
		||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
			
		||||
		rs.ExitCode = 1
 | 
			
		||||
	} else {
 | 
			
		||||
		// this updates ExitCode
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(rs))
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rs.ExitCode != 0 {
 | 
			
		||||
 | 
			
		||||
@ -62,8 +62,8 @@ def check_state(name, enablements):
 | 
			
		||||
 | 
			
		||||
    config = instance['config']
 | 
			
		||||
 | 
			
		||||
    if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
 | 
			
		||||
        raise Exception(f"unexpected args {instance['config']['args']}")
 | 
			
		||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['command'][0]):
 | 
			
		||||
        raise Exception(f"unexpected command {instance['config']['command']}")
 | 
			
		||||
 | 
			
		||||
    if config['confinement']['enablements'] != enablements:
 | 
			
		||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
			
		||||
 | 
			
		||||
@ -6,19 +6,18 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func withNixDaemon(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
			
		||||
	app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
			
		||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
			
		||||
) {
 | 
			
		||||
	mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
			
		||||
		ID:   app.ID,
 | 
			
		||||
		Path: shellPath,
 | 
			
		||||
		Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
			
		||||
		ID: app.ID,
 | 
			
		||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
			
		||||
			// start nix-daemon
 | 
			
		||||
			"nix-daemon --store / & " +
 | 
			
		||||
			// wait for socket to appear
 | 
			
		||||
@ -35,11 +34,11 @@ func withNixDaemon(
 | 
			
		||||
			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,
 | 
			
		||||
				Seccomp:  seccomp.FlagMultiarch,
 | 
			
		||||
				Tty:      dropShell,
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				UserNS:       true, // nix sandbox requires userns
 | 
			
		||||
				Net:          net,
 | 
			
		||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
			
		||||
				},
 | 
			
		||||
@ -62,20 +61,19 @@ func withNixDaemon(
 | 
			
		||||
func withCacheDir(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	action string, command []string, workDir string,
 | 
			
		||||
	app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
			
		||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
			
		||||
	mustRunAppDropShell(ctx, &fst.Config{
 | 
			
		||||
		ID:   app.ID,
 | 
			
		||||
		Path: shellPath,
 | 
			
		||||
		Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
			
		||||
		ID:      app.ID,
 | 
			
		||||
		Command: []string{shellPath, "-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 shim
 | 
			
		||||
			Sandbox: &fst.SandboxConfig{
 | 
			
		||||
				Hostname: formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				Seccomp:  seccomp.FlagMultiarch,
 | 
			
		||||
				Tty:      dropShell,
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
			
		||||
					{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
			
		||||
@ -99,7 +97,7 @@ func withCacheDir(
 | 
			
		||||
 | 
			
		||||
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
 | 
			
		||||
	if dropShell {
 | 
			
		||||
		config.Args = []string{shellPath, "-l"}
 | 
			
		||||
		config.Command = []string{shellPath, "-l"}
 | 
			
		||||
		mustRunApp(ctx, config, beforeFail)
 | 
			
		||||
		beforeFail()
 | 
			
		||||
		internal.Exit(0)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@ -7,11 +7,11 @@
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1742234739,
 | 
			
		||||
        "narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
 | 
			
		||||
        "lastModified": 1739757849,
 | 
			
		||||
        "narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=",
 | 
			
		||||
        "owner": "nix-community",
 | 
			
		||||
        "repo": "home-manager",
 | 
			
		||||
        "rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
 | 
			
		||||
        "rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
@ -23,11 +23,11 @@
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1742512142,
 | 
			
		||||
        "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
 | 
			
		||||
        "lastModified": 1741445498,
 | 
			
		||||
        "narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
 | 
			
		||||
        "rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										100
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								flake.nix
									
									
									
									
									
								
							@ -27,7 +27,7 @@
 | 
			
		||||
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
			
		||||
    in
 | 
			
		||||
    {
 | 
			
		||||
      nixosModules.fortify = import ./nixos.nix self.packages;
 | 
			
		||||
      nixosModules.fortify = import ./nixos.nix;
 | 
			
		||||
 | 
			
		||||
      buildPackage = forAllSystems (
 | 
			
		||||
        system:
 | 
			
		||||
@ -105,21 +105,9 @@
 | 
			
		||||
          default = fortify;
 | 
			
		||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
			
		||||
            inherit (pkgs)
 | 
			
		||||
              # passthru.buildInputs
 | 
			
		||||
              go
 | 
			
		||||
              gcc
 | 
			
		||||
 | 
			
		||||
              # nativeBuildInputs
 | 
			
		||||
              pkg-config
 | 
			
		||||
              wayland-scanner
 | 
			
		||||
              makeBinaryWrapper
 | 
			
		||||
 | 
			
		||||
              # appPackages
 | 
			
		||||
              glibc
 | 
			
		||||
              bubblewrap
 | 
			
		||||
              xdg-dbus-proxy
 | 
			
		||||
 | 
			
		||||
              # fpkg
 | 
			
		||||
              glibc
 | 
			
		||||
              zstd
 | 
			
		||||
              gnutar
 | 
			
		||||
              coreutils
 | 
			
		||||
@ -127,7 +115,7 @@
 | 
			
		||||
          };
 | 
			
		||||
          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
			
		||||
 | 
			
		||||
          dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
 | 
			
		||||
          dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
 | 
			
		||||
            # go requires XDG_CACHE_HOME for the build cache
 | 
			
		||||
            export XDG_CACHE_HOME="$(mktemp -d)"
 | 
			
		||||
 | 
			
		||||
@ -140,21 +128,93 @@
 | 
			
		||||
            export FORTIFY_VERSION="v${fortify.version}"
 | 
			
		||||
            ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
			
		||||
          '';
 | 
			
		||||
 | 
			
		||||
          fhs = pkgs.buildFHSEnv {
 | 
			
		||||
            pname = "fortify-fhs";
 | 
			
		||||
            inherit (fortify) version;
 | 
			
		||||
            targetPkgs =
 | 
			
		||||
              pkgs:
 | 
			
		||||
              with pkgs;
 | 
			
		||||
              [
 | 
			
		||||
                go
 | 
			
		||||
                gcc
 | 
			
		||||
                pkg-config
 | 
			
		||||
                wayland-scanner
 | 
			
		||||
              ]
 | 
			
		||||
              ++ (
 | 
			
		||||
                with pkgs.pkgsStatic;
 | 
			
		||||
                [
 | 
			
		||||
                  musl
 | 
			
		||||
                  libffi
 | 
			
		||||
                  libseccomp
 | 
			
		||||
                  acl
 | 
			
		||||
                  wayland
 | 
			
		||||
                  wayland-protocols
 | 
			
		||||
                ]
 | 
			
		||||
                ++ (with xorg; [
 | 
			
		||||
                  libxcb
 | 
			
		||||
                  libXau
 | 
			
		||||
                  libXdmcp
 | 
			
		||||
 | 
			
		||||
                  xorgproto
 | 
			
		||||
                ])
 | 
			
		||||
              );
 | 
			
		||||
            extraOutputsToInstall = [ "dev" ];
 | 
			
		||||
            profile = ''
 | 
			
		||||
              export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
 | 
			
		||||
            '';
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      devShells = forAllSystems (
 | 
			
		||||
        system:
 | 
			
		||||
        let
 | 
			
		||||
          inherit (self.packages.${system}) fortify;
 | 
			
		||||
          inherit (self.packages.${system}) fortify fhs;
 | 
			
		||||
          pkgs = nixpkgsFor.${system};
 | 
			
		||||
        in
 | 
			
		||||
        {
 | 
			
		||||
          default = pkgs.mkShell { buildInputs = fortify.targetPkgs; };
 | 
			
		||||
          withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
 | 
			
		||||
          default = pkgs.mkShell {
 | 
			
		||||
            buildInputs =
 | 
			
		||||
              with pkgs;
 | 
			
		||||
              [
 | 
			
		||||
                go
 | 
			
		||||
                gcc
 | 
			
		||||
              ]
 | 
			
		||||
              # buildInputs
 | 
			
		||||
              ++ (
 | 
			
		||||
                with pkgsStatic;
 | 
			
		||||
                [
 | 
			
		||||
                  musl
 | 
			
		||||
                  libffi
 | 
			
		||||
                  libseccomp
 | 
			
		||||
                  acl
 | 
			
		||||
                  wayland
 | 
			
		||||
                  wayland-protocols
 | 
			
		||||
                ]
 | 
			
		||||
                ++ (with xorg; [
 | 
			
		||||
                  libxcb
 | 
			
		||||
                  libXau
 | 
			
		||||
                  libXdmcp
 | 
			
		||||
                ])
 | 
			
		||||
              )
 | 
			
		||||
              # nativeBuildInputs
 | 
			
		||||
              ++ [
 | 
			
		||||
                pkg-config
 | 
			
		||||
                wayland-scanner
 | 
			
		||||
                makeBinaryWrapper
 | 
			
		||||
              ];
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          fhs = fhs.env;
 | 
			
		||||
 | 
			
		||||
          withPackage = nixpkgsFor.${system}.mkShell {
 | 
			
		||||
            buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          generateDoc =
 | 
			
		||||
            let
 | 
			
		||||
              pkgs = nixpkgsFor.${system};
 | 
			
		||||
              inherit (pkgs) lib;
 | 
			
		||||
 | 
			
		||||
              doc =
 | 
			
		||||
@ -163,7 +223,7 @@
 | 
			
		||||
                    specialArgs = {
 | 
			
		||||
                      inherit pkgs;
 | 
			
		||||
                    };
 | 
			
		||||
                    modules = [ (import ./options.nix self.packages) ];
 | 
			
		||||
                    modules = [ ./options.nix ];
 | 
			
		||||
                  };
 | 
			
		||||
                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
			
		||||
                in
 | 
			
		||||
@ -173,7 +233,7 @@
 | 
			
		||||
                sed -i '/*Declared by:*/,+1 d' $out
 | 
			
		||||
              '';
 | 
			
		||||
            in
 | 
			
		||||
            pkgs.mkShell {
 | 
			
		||||
            nixpkgsFor.${system}.mkShell {
 | 
			
		||||
              shellHook = ''
 | 
			
		||||
                exec cat ${docText} > options.md
 | 
			
		||||
              '';
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -18,7 +19,7 @@ type App interface {
 | 
			
		||||
 | 
			
		||||
type SealedApp interface {
 | 
			
		||||
	// Run commits sealed system setup and starts the app process.
 | 
			
		||||
	Run(rs *RunState) error
 | 
			
		||||
	Run(ctx context.Context, rs *RunState) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RunState stores the outcome of a call to [SealedApp.Run].
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -14,11 +14,8 @@ type Config struct {
 | 
			
		||||
	// passed to wayland security-context-v1 as application ID
 | 
			
		||||
	// and used as part of defaults in dbus session proxy
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// absolute path to executable file
 | 
			
		||||
	Path string `json:"path,omitempty"`
 | 
			
		||||
	// final args passed to container init
 | 
			
		||||
	Args []string `json:"args"`
 | 
			
		||||
	// final argv, passed to init
 | 
			
		||||
	Command []string `json:"command"`
 | 
			
		||||
 | 
			
		||||
	Confinement ConfinementConfig `json:"confinement"`
 | 
			
		||||
}
 | 
			
		||||
@ -29,13 +26,13 @@ type ConfinementConfig struct {
 | 
			
		||||
	AppID int `json:"app_id"`
 | 
			
		||||
	// list of supplementary groups to inherit
 | 
			
		||||
	Groups []string `json:"groups"`
 | 
			
		||||
	// passwd username in container, defaults to passwd name of target uid or chronos
 | 
			
		||||
	// passwd username in the sandbox, defaults to passwd name of target uid or chronos
 | 
			
		||||
	Username string `json:"username,omitempty"`
 | 
			
		||||
	// home directory in container, empty for outer
 | 
			
		||||
	// home directory in sandbox, empty for outer
 | 
			
		||||
	Inner string `json:"home_inner"`
 | 
			
		||||
	// home directory in init namespace
 | 
			
		||||
	Outer string `json:"home"`
 | 
			
		||||
	// abstract sandbox configuration
 | 
			
		||||
	// bwrap sandbox confinement configuration
 | 
			
		||||
	Sandbox *SandboxConfig `json:"sandbox"`
 | 
			
		||||
	// extra acl ops, runs after everything else
 | 
			
		||||
	ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
			
		||||
@ -47,7 +44,7 @@ type ConfinementConfig struct {
 | 
			
		||||
	// nil value makes session bus proxy assume built-in defaults
 | 
			
		||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// system resources to expose to the container
 | 
			
		||||
	// system resources to expose to the sandbox
 | 
			
		||||
	Enablements system.Enablements `json:"enablements"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -79,12 +76,24 @@ func (e *ExtraPermConfig) String() string {
 | 
			
		||||
	return string(buf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FilesystemConfig struct {
 | 
			
		||||
	// mount point in sandbox, same as src if empty
 | 
			
		||||
	Dst string `json:"dst,omitempty"`
 | 
			
		||||
	// host filesystem path to make available to sandbox
 | 
			
		||||
	Src string `json:"src"`
 | 
			
		||||
	// write access
 | 
			
		||||
	Write bool `json:"write,omitempty"`
 | 
			
		||||
	// device access
 | 
			
		||||
	Device bool `json:"dev,omitempty"`
 | 
			
		||||
	// fail if mount fails
 | 
			
		||||
	Must bool `json:"require,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Template returns a fully populated instance of Config.
 | 
			
		||||
func Template() *Config {
 | 
			
		||||
	return &Config{
 | 
			
		||||
		ID:   "org.chromium.Chromium",
 | 
			
		||||
		Path: "/run/current-system/sw/bin/chromium",
 | 
			
		||||
		Args: []string{
 | 
			
		||||
		ID: "org.chromium.Chromium",
 | 
			
		||||
		Command: []string{
 | 
			
		||||
			"chromium",
 | 
			
		||||
			"--ignore-gpu-blocklist",
 | 
			
		||||
			"--disable-smooth-scrolling",
 | 
			
		||||
@ -99,13 +108,11 @@ func Template() *Config {
 | 
			
		||||
			Inner:    "/var/lib/fortify",
 | 
			
		||||
			Sandbox: &SandboxConfig{
 | 
			
		||||
				Hostname:      "localhost",
 | 
			
		||||
				Devel:         true,
 | 
			
		||||
				Userns:        true,
 | 
			
		||||
				UserNS:        true,
 | 
			
		||||
				Net:           true,
 | 
			
		||||
				Dev:           true,
 | 
			
		||||
				Seccomp:       seccomp.FlagMultiarch,
 | 
			
		||||
				Tty:           true,
 | 
			
		||||
				Multiarch:     true,
 | 
			
		||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
			
		||||
				NoNewSession:  true,
 | 
			
		||||
				MapRealUID:    true,
 | 
			
		||||
				DirectWayland: false,
 | 
			
		||||
				// example API credentials pulled from Google Chrome
 | 
			
		||||
@ -124,10 +131,10 @@ func Template() *Config {
 | 
			
		||||
						Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
			
		||||
					{Src: "/dev/dri", Device: true},
 | 
			
		||||
				},
 | 
			
		||||
				Link:    [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
			
		||||
				Etc:     "/etc",
 | 
			
		||||
				AutoEtc: true,
 | 
			
		||||
				Cover:   []string{"/var/run/nscd"},
 | 
			
		||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
			
		||||
				Etc:      "/etc",
 | 
			
		||||
				AutoEtc:  true,
 | 
			
		||||
				Override: []string{"/var/run/nscd"},
 | 
			
		||||
			},
 | 
			
		||||
			ExtraPerms: []*ExtraPermConfig{
 | 
			
		||||
				{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							
							
						
						
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							@ -4,149 +4,125 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"maps"
 | 
			
		||||
	"path"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SandboxConfig describes resources made available to the sandbox.
 | 
			
		||||
type (
 | 
			
		||||
	SandboxConfig struct {
 | 
			
		||||
		// container hostname
 | 
			
		||||
		Hostname string `json:"hostname,omitempty"`
 | 
			
		||||
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"`
 | 
			
		||||
	// seccomp syscall filter policy
 | 
			
		||||
	Syscall *bwrap.SyscallPolicy `json:"syscall"`
 | 
			
		||||
	// do not run in new session
 | 
			
		||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
			
		||||
	// map target user uid to privileged user uid in the user namespace
 | 
			
		||||
	MapRealUID bool `json:"map_real_uid"`
 | 
			
		||||
	// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
			
		||||
	// and the bare socket is mounted to the sandbox
 | 
			
		||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
			
		||||
 | 
			
		||||
		// extra seccomp flags
 | 
			
		||||
		Seccomp seccomp.SyscallOpts `json:"seccomp"`
 | 
			
		||||
		// allow ptrace and friends
 | 
			
		||||
		Devel bool `json:"devel,omitempty"`
 | 
			
		||||
		// allow userns creation in container
 | 
			
		||||
		Userns bool `json:"userns,omitempty"`
 | 
			
		||||
		// share host net namespace
 | 
			
		||||
		Net bool `json:"net,omitempty"`
 | 
			
		||||
		// expose main process tty
 | 
			
		||||
		Tty bool `json:"tty,omitempty"`
 | 
			
		||||
		// allow multiarch
 | 
			
		||||
		Multiarch bool `json:"multiarch,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"`
 | 
			
		||||
	// mount tmpfs over these paths,
 | 
			
		||||
	// runs right before [ConfinementConfig.ExtraPerms]
 | 
			
		||||
	Override []string `json:"override"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		// initial process environment variables
 | 
			
		||||
		Env map[string]string `json:"env"`
 | 
			
		||||
		// map target user uid to privileged user uid in the user namespace
 | 
			
		||||
		MapRealUID bool `json:"map_real_uid"`
 | 
			
		||||
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
 | 
			
		||||
type SandboxSys interface {
 | 
			
		||||
	Geteuid() int
 | 
			
		||||
	Paths() Paths
 | 
			
		||||
	ReadDir(name string) ([]fs.DirEntry, error)
 | 
			
		||||
	EvalSymlinks(path string) (string, error)
 | 
			
		||||
 | 
			
		||||
		// expose all devices
 | 
			
		||||
		Dev bool `json:"dev,omitempty"`
 | 
			
		||||
		// container host filesystem bind mounts
 | 
			
		||||
		Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
			
		||||
		// create symlinks inside container filesystem
 | 
			
		||||
		Link [][2]string `json:"symlink"`
 | 
			
		||||
	Println(v ...any)
 | 
			
		||||
	Printf(format string, v ...any)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
			
		||||
		// and the bare socket is mounted to the sandbox
 | 
			
		||||
		DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
			
		||||
 | 
			
		||||
		// read-only /etc directory
 | 
			
		||||
		Etc string `json:"etc,omitempty"`
 | 
			
		||||
		// automatically set up /etc symlinks
 | 
			
		||||
		AutoEtc bool `json:"auto_etc"`
 | 
			
		||||
		// cover these paths or create them if they do not already exist
 | 
			
		||||
		Cover []string `json:"cover"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
 | 
			
		||||
	SandboxSys interface {
 | 
			
		||||
		Getuid() int
 | 
			
		||||
		Getgid() int
 | 
			
		||||
		Paths() Paths
 | 
			
		||||
		ReadDir(name string) ([]fs.DirEntry, error)
 | 
			
		||||
		EvalSymlinks(path string) (string, error)
 | 
			
		||||
 | 
			
		||||
		Println(v ...any)
 | 
			
		||||
		Printf(format string, v ...any)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FilesystemConfig is a representation of [sandbox.BindMount].
 | 
			
		||||
	FilesystemConfig struct {
 | 
			
		||||
		// mount point in container, same as src if empty
 | 
			
		||||
		Dst string `json:"dst,omitempty"`
 | 
			
		||||
		// host filesystem path to make available to the container
 | 
			
		||||
		Src string `json:"src"`
 | 
			
		||||
		// do not mount filesystem read-only
 | 
			
		||||
		Write bool `json:"write,omitempty"`
 | 
			
		||||
		// do not disable device files
 | 
			
		||||
		Device bool `json:"dev,omitempty"`
 | 
			
		||||
		// fail if the bind mount cannot be established for any reason
 | 
			
		||||
		Must bool `json:"require,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
 | 
			
		||||
// Note that remaining container setup must be queued by the [App] implementation.
 | 
			
		||||
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
 | 
			
		||||
// 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(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return nil, nil, syscall.EBADE
 | 
			
		||||
		return nil, errors.New("nil sandbox config")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	container := &sandbox.Params{
 | 
			
		||||
		Hostname: s.Hostname,
 | 
			
		||||
		Ops:      new(sandbox.Ops),
 | 
			
		||||
		Seccomp:  s.Seccomp,
 | 
			
		||||
	if s.Syscall == nil {
 | 
			
		||||
		sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* 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 */
 | 
			
		||||
	*container.Ops = slices.Grow(*container.Ops, 1<<8)
 | 
			
		||||
 | 
			
		||||
	if s.Devel {
 | 
			
		||||
		container.Flags |= sandbox.FAllowDevel
 | 
			
		||||
	}
 | 
			
		||||
	if s.Userns {
 | 
			
		||||
		container.Flags |= sandbox.FAllowUserns
 | 
			
		||||
	}
 | 
			
		||||
	if s.Net {
 | 
			
		||||
		container.Flags |= sandbox.FAllowNet
 | 
			
		||||
	}
 | 
			
		||||
	if s.Tty {
 | 
			
		||||
		container.Flags |= sandbox.FAllowTTY
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.MapRealUID {
 | 
			
		||||
		/* some programs fail to connect to dbus session running as a different uid
 | 
			
		||||
		so this workaround is introduced to map priv-side caller uid in container */
 | 
			
		||||
		container.Uid = sys.Getuid()
 | 
			
		||||
		*uid = container.Uid
 | 
			
		||||
		container.Gid = sys.Getgid()
 | 
			
		||||
		*gid = container.Gid
 | 
			
		||||
	if !s.MapRealUID {
 | 
			
		||||
		// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
 | 
			
		||||
		*uid = 65534
 | 
			
		||||
	} else {
 | 
			
		||||
		*uid = sandbox.OverflowUid()
 | 
			
		||||
		*gid = sandbox.OverflowGid()
 | 
			
		||||
		// some programs fail to connect to dbus session running as a different uid, so a separate workaround
 | 
			
		||||
		// is introduced to map priv-side caller uid in namespace
 | 
			
		||||
		*uid = sys.Geteuid()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	container.
 | 
			
		||||
		Proc("/proc").
 | 
			
		||||
		Tmpfs(Tmp, 1<<12, 0755)
 | 
			
		||||
	conf := (&bwrap.Config{
 | 
			
		||||
		Net:      s.Net,
 | 
			
		||||
		UserNS:   s.UserNS,
 | 
			
		||||
		UID:      uid,
 | 
			
		||||
		GID:      uid,
 | 
			
		||||
		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),
 | 
			
		||||
 | 
			
		||||
		Syscall:       s.Syscall,
 | 
			
		||||
		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),
 | 
			
		||||
	}).
 | 
			
		||||
		Procfs("/proc").
 | 
			
		||||
		Tmpfs(Tmp, 4*1024)
 | 
			
		||||
 | 
			
		||||
	if !s.Dev {
 | 
			
		||||
		container.Dev("/dev").Mqueue("/dev/mqueue")
 | 
			
		||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
			
		||||
	} else {
 | 
			
		||||
		container.Bind("/dev", "/dev", sandbox.BindDevice)
 | 
			
		||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* retrieve paths and hide them if they're made available in the sandbox;
 | 
			
		||||
	this feature tries to improve user experience of permissive defaults, and
 | 
			
		||||
	to warn about issues in custom configuration; it is NOT a security feature
 | 
			
		||||
	and should not be treated as such, ALWAYS be careful with what you bind */
 | 
			
		||||
	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 := sys.Paths()
 | 
			
		||||
	hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
			
		||||
	_, systemBusAddr := dbus.Address()
 | 
			
		||||
	if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		// there is usually only one, do not preallocate
 | 
			
		||||
		for _, entry := range entries {
 | 
			
		||||
@ -172,7 +148,7 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
 | 
			
		||||
	hidePathMatch := make([]bool, len(hidePaths))
 | 
			
		||||
	for i := range hidePaths {
 | 
			
		||||
		if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -182,19 +158,19 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !path.IsAbs(c.Src) {
 | 
			
		||||
			return nil, nil, fmt.Errorf("src path %q is not absolute", 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, nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
			
		||||
			return nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		srcH := c.Src
 | 
			
		||||
		if err := evalSymlinks(sys, &srcH); err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for i := range hidePaths {
 | 
			
		||||
@ -204,71 +180,54 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
				return nil, err
 | 
			
		||||
			} else if ok {
 | 
			
		||||
				hidePathMatch[i] = true
 | 
			
		||||
				sys.Printf("hiding paths from %q", c.Src)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var flags int
 | 
			
		||||
		if c.Write {
 | 
			
		||||
			flags |= sandbox.BindWritable
 | 
			
		||||
		}
 | 
			
		||||
		if c.Device {
 | 
			
		||||
			flags |= sandbox.BindDevice | sandbox.BindWritable
 | 
			
		||||
		}
 | 
			
		||||
		if !c.Must {
 | 
			
		||||
			flags |= sandbox.BindOptional
 | 
			
		||||
		}
 | 
			
		||||
		container.Bind(c.Src, dest, flags)
 | 
			
		||||
		conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// cover matched paths
 | 
			
		||||
	// hide marked paths before setting up shares
 | 
			
		||||
	for i, ok := range hidePathMatch {
 | 
			
		||||
		if ok {
 | 
			
		||||
			container.Tmpfs(hidePaths[i], 1<<13, 0755)
 | 
			
		||||
			conf.Tmpfs(hidePaths[i], 8192)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, l := range s.Link {
 | 
			
		||||
		container.Link(l[0], l[1])
 | 
			
		||||
		conf.Symlink(l[0], l[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// perf: this might work better if implemented as a setup op in container init
 | 
			
		||||
	if !s.AutoEtc {
 | 
			
		||||
		if s.Etc != "" {
 | 
			
		||||
			container.Bind(s.Etc, "/etc", 0)
 | 
			
		||||
	if s.AutoEtc {
 | 
			
		||||
		etc := s.Etc
 | 
			
		||||
		if etc == "" {
 | 
			
		||||
			etc = "/etc"
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		etcPath := s.Etc
 | 
			
		||||
		if etcPath == "" {
 | 
			
		||||
			etcPath = "/etc"
 | 
			
		||||
		}
 | 
			
		||||
		container.
 | 
			
		||||
			Bind(etcPath, Tmp+"/etc", 0).
 | 
			
		||||
			Mkdir("/etc", 0700)
 | 
			
		||||
		conf.Bind(etc, Tmp+"/etc")
 | 
			
		||||
 | 
			
		||||
		// link host /etc contents to prevent dropping passwd/group bind mounts
 | 
			
		||||
		if d, err := sys.ReadDir(etcPath); err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
			
		||||
		if d, err := sys.ReadDir(etc); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, ent := range d {
 | 
			
		||||
				n := ent.Name()
 | 
			
		||||
				switch n {
 | 
			
		||||
				name := ent.Name()
 | 
			
		||||
				switch name {
 | 
			
		||||
				case "passwd":
 | 
			
		||||
				case "group":
 | 
			
		||||
 | 
			
		||||
				case "mtab":
 | 
			
		||||
					container.Link("/proc/mounts", "/etc/"+n)
 | 
			
		||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
			
		||||
				default:
 | 
			
		||||
					container.Link(Tmp+"/etc/"+n, "/etc/"+n)
 | 
			
		||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return container, maps.Clone(s.Env), nil
 | 
			
		||||
	return conf, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func evalSymlinks(sys SandboxSys, v *string) error {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
@ -62,7 +61,7 @@ func (h *helperContainer) Start() error {
 | 
			
		||||
		h.Env = append(h.Env, FortifyStatus+"=1")
 | 
			
		||||
 | 
			
		||||
		// stat is populated on fulfill
 | 
			
		||||
		h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
 | 
			
		||||
		h.Cancel = func() error { return h.stat.Close() }
 | 
			
		||||
	} else {
 | 
			
		||||
		h.Env = append(h.Env, FortifyStatus+"=0")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"sync"
 | 
			
		||||
@ -11,10 +10,9 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func New(ctx context.Context, os sys.State) (fst.App, error) {
 | 
			
		||||
func New(os sys.State) (fst.App, error) {
 | 
			
		||||
	a := new(app)
 | 
			
		||||
	a.sys = os
 | 
			
		||||
	a.ctx = ctx
 | 
			
		||||
 | 
			
		||||
	id := new(fst.ID)
 | 
			
		||||
	err := fst.NewAppID(id)
 | 
			
		||||
@ -23,8 +21,8 @@ func New(ctx context.Context, os sys.State) (fst.App, error) {
 | 
			
		||||
	return a, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustNew(ctx context.Context, os sys.State) fst.App {
 | 
			
		||||
	a, err := New(ctx, os)
 | 
			
		||||
func MustNew(os sys.State) fst.App {
 | 
			
		||||
	a, err := New(os)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("cannot create app: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@ -34,7 +32,6 @@ func MustNew(ctx context.Context, os sys.State) fst.App {
 | 
			
		||||
type app struct {
 | 
			
		||||
	id  *stringPair[fst.ID]
 | 
			
		||||
	sys sys.State
 | 
			
		||||
	ctx context.Context
 | 
			
		||||
 | 
			
		||||
	*outcome
 | 
			
		||||
	mu sync.RWMutex
 | 
			
		||||
@ -74,7 +71,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
 | 
			
		||||
 | 
			
		||||
	seal := new(outcome)
 | 
			
		||||
	seal.id = a.id
 | 
			
		||||
	err := seal.finalise(a.ctx, a.sys, config)
 | 
			
		||||
	err := seal.finalise(a.sys, config)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		a.outcome = seal
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -12,20 +12,20 @@ var testCasesNixos = []sealTestCase{
 | 
			
		||||
	{
 | 
			
		||||
		"nixos chromium direct wayland", new(stubNixOS),
 | 
			
		||||
		&fst.Config{
 | 
			
		||||
			ID:   "org.chromium.Chromium",
 | 
			
		||||
			Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
			
		||||
			ID:      "org.chromium.Chromium",
 | 
			
		||||
			Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
			
		||||
			Confinement: fst.ConfinementConfig{
 | 
			
		||||
				AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
			
		||||
				Outer: "/var/lib/persist/module/fortify/0/1",
 | 
			
		||||
				Sandbox: &fst.SandboxConfig{
 | 
			
		||||
					Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
			
		||||
					UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
			
		||||
					Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
						{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
			
		||||
						{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
			
		||||
						{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
			
		||||
						{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
			
		||||
					},
 | 
			
		||||
					Cover: []string{"/var/run/nscd"},
 | 
			
		||||
					Override: []string{"/var/run/nscd"},
 | 
			
		||||
				},
 | 
			
		||||
				SystemBus: &dbus.Config{
 | 
			
		||||
					Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
			
		||||
@ -88,133 +88,136 @@ var testCasesNixos = []sealTestCase{
 | 
			
		||||
			}).
 | 
			
		||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
			
		||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
			
		||||
		&sandbox.Params{
 | 
			
		||||
			Uid:   1971,
 | 
			
		||||
			Gid:   100,
 | 
			
		||||
			Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
 | 
			
		||||
			Dir:   "/var/lib/persist/module/fortify/0/1",
 | 
			
		||||
			Path:  "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
			
		||||
			Args:  []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
			
		||||
			Env: []string{
 | 
			
		||||
				"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
 | 
			
		||||
				"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
			
		||||
				"HOME=/var/lib/persist/module/fortify/0/1",
 | 
			
		||||
				"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
			
		||||
				"PULSE_SERVER=unix:/run/user/1971/pulse/native",
 | 
			
		||||
				"TERM=xterm-256color",
 | 
			
		||||
				"USER=u0_a1",
 | 
			
		||||
				"WAYLAND_DISPLAY=wayland-0",
 | 
			
		||||
				"XDG_RUNTIME_DIR=/run/user/1971",
 | 
			
		||||
				"XDG_SESSION_CLASS=user",
 | 
			
		||||
				"XDG_SESSION_TYPE=tty",
 | 
			
		||||
		(&bwrap.Config{
 | 
			
		||||
			Net:      true,
 | 
			
		||||
			UserNS:   true,
 | 
			
		||||
			Chdir:    "/var/lib/persist/module/fortify/0/1",
 | 
			
		||||
			Clearenv: true,
 | 
			
		||||
			SetEnv: map[string]string{
 | 
			
		||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
 | 
			
		||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
			
		||||
				"HOME":                     "/var/lib/persist/module/fortify/0/1",
 | 
			
		||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
			
		||||
				"PULSE_SERVER":             "unix:/run/user/1971/pulse/native",
 | 
			
		||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
			
		||||
				"TERM":                     "xterm-256color",
 | 
			
		||||
				"USER":                     "u0_a1",
 | 
			
		||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
			
		||||
				"XDG_RUNTIME_DIR":          "/run/user/1971",
 | 
			
		||||
				"XDG_SESSION_CLASS":        "user",
 | 
			
		||||
				"XDG_SESSION_TYPE":         "tty",
 | 
			
		||||
			},
 | 
			
		||||
			Ops: new(sandbox.Ops).
 | 
			
		||||
				Proc("/proc").
 | 
			
		||||
				Tmpfs(fst.Tmp, 4096, 0755).
 | 
			
		||||
				Dev("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
				Bind("/bin", "/bin", 0).
 | 
			
		||||
				Bind("/usr/bin", "/usr/bin", 0).
 | 
			
		||||
				Bind("/nix/store", "/nix/store", 0).
 | 
			
		||||
				Bind("/run/current-system", "/run/current-system", 0).
 | 
			
		||||
				Bind("/sys/block", "/sys/block", sandbox.BindOptional).
 | 
			
		||||
				Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
 | 
			
		||||
				Bind("/sys/class", "/sys/class", sandbox.BindOptional).
 | 
			
		||||
				Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
 | 
			
		||||
				Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
 | 
			
		||||
				Bind("/run/opengl-driver", "/run/opengl-driver", 0).
 | 
			
		||||
				Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
 | 
			
		||||
				Bind("/etc", fst.Tmp+"/etc", 0).
 | 
			
		||||
				Mkdir("/etc", 0700).
 | 
			
		||||
				Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
				Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
				Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
				Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
				Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
				Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
				Link("/proc/mounts", "/etc/mtab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
				Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
				Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
				Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
				Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
				Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
				Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
				Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
				Tmpfs("/run/user", 4096, 0755).
 | 
			
		||||
				Tmpfs("/run/user/1971", 8388608, 0755).
 | 
			
		||||
				Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
 | 
			
		||||
				Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
 | 
			
		||||
				Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
				Place("/etc/group", []byte("fortify:x:100:\n")).
 | 
			
		||||
				Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
 | 
			
		||||
				Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
 | 
			
		||||
				Place(fst.Tmp+"/pulse-cookie", nil).
 | 
			
		||||
				Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
 | 
			
		||||
				Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
			
		||||
				Tmpfs("/var/run/nscd", 8192, 0755),
 | 
			
		||||
		},
 | 
			
		||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
			
		||||
			NewSession:    true,
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
			AsInit:        true,
 | 
			
		||||
		}).SetUID(1971).SetGID(1971).
 | 
			
		||||
			Procfs("/proc").
 | 
			
		||||
			Tmpfs(fst.Tmp, 4096).
 | 
			
		||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
			Bind("/bin", "/bin").
 | 
			
		||||
			Bind("/usr/bin", "/usr/bin").
 | 
			
		||||
			Bind("/nix/store", "/nix/store").
 | 
			
		||||
			Bind("/run/current-system", "/run/current-system").
 | 
			
		||||
			Bind("/sys/block", "/sys/block", true).
 | 
			
		||||
			Bind("/sys/bus", "/sys/bus", true).
 | 
			
		||||
			Bind("/sys/class", "/sys/class", true).
 | 
			
		||||
			Bind("/sys/dev", "/sys/dev", true).
 | 
			
		||||
			Bind("/sys/devices", "/sys/devices", true).
 | 
			
		||||
			Bind("/run/opengl-driver", "/run/opengl-driver").
 | 
			
		||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
			
		||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
			Tmpfs("/run/user", 1048576).
 | 
			
		||||
			Tmpfs("/run/user/1971", 8388608).
 | 
			
		||||
			Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
 | 
			
		||||
			Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
 | 
			
		||||
			CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
			CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
 | 
			
		||||
			Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
 | 
			
		||||
			Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
 | 
			
		||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
			
		||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
 | 
			
		||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
			
		||||
			Tmpfs("/var/run/nscd", 8192).
 | 
			
		||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
			
		||||
			Symlink("fortify", "/.fortify/sbin/init0"),
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
	{
 | 
			
		||||
		"nixos permissive defaults no enablements", new(stubNixOS),
 | 
			
		||||
		&fst.Config{
 | 
			
		||||
			Command: make([]string, 0),
 | 
			
		||||
			Confinement: fst.ConfinementConfig{
 | 
			
		||||
				AppID:    0,
 | 
			
		||||
				Username: "chronos",
 | 
			
		||||
@ -34,132 +35,136 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
			
		||||
			Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
			
		||||
			Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
			
		||||
		&sandbox.Params{
 | 
			
		||||
			Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
			
		||||
			Dir:   "/home/chronos",
 | 
			
		||||
			Path:  "/run/current-system/sw/bin/zsh",
 | 
			
		||||
			Args:  []string{"/run/current-system/sw/bin/zsh"},
 | 
			
		||||
			Env: []string{
 | 
			
		||||
				"HOME=/home/chronos",
 | 
			
		||||
				"TERM=xterm-256color",
 | 
			
		||||
				"USER=chronos",
 | 
			
		||||
				"XDG_RUNTIME_DIR=/run/user/65534",
 | 
			
		||||
				"XDG_SESSION_CLASS=user",
 | 
			
		||||
				"XDG_SESSION_TYPE=tty",
 | 
			
		||||
			},
 | 
			
		||||
			Ops: new(sandbox.Ops).
 | 
			
		||||
				Proc("/proc").
 | 
			
		||||
				Tmpfs(fst.Tmp, 4096, 0755).
 | 
			
		||||
				Dev("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
				Bind("/bin", "/bin", sandbox.BindWritable).
 | 
			
		||||
				Bind("/boot", "/boot", sandbox.BindWritable).
 | 
			
		||||
				Bind("/home", "/home", sandbox.BindWritable).
 | 
			
		||||
				Bind("/lib", "/lib", sandbox.BindWritable).
 | 
			
		||||
				Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
			
		||||
				Bind("/nix", "/nix", sandbox.BindWritable).
 | 
			
		||||
				Bind("/root", "/root", sandbox.BindWritable).
 | 
			
		||||
				Bind("/run", "/run", sandbox.BindWritable).
 | 
			
		||||
				Bind("/srv", "/srv", sandbox.BindWritable).
 | 
			
		||||
				Bind("/sys", "/sys", sandbox.BindWritable).
 | 
			
		||||
				Bind("/usr", "/usr", sandbox.BindWritable).
 | 
			
		||||
				Bind("/var", "/var", sandbox.BindWritable).
 | 
			
		||||
				Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
			
		||||
				Tmpfs("/run/user/1971", 8192, 0755).
 | 
			
		||||
				Tmpfs("/run/dbus", 8192, 0755).
 | 
			
		||||
				Bind("/etc", fst.Tmp+"/etc", 0).
 | 
			
		||||
				Mkdir("/etc", 0700).
 | 
			
		||||
				Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
				Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
				Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
				Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
				Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
				Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
				Link("/proc/mounts", "/etc/mtab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
				Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
				Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
				Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
				Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
				Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
				Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
				Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
				Tmpfs("/run/user", 4096, 0755).
 | 
			
		||||
				Tmpfs("/run/user/65534", 8388608, 0755).
 | 
			
		||||
				Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
 | 
			
		||||
				Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
			
		||||
				Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
				Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
			
		||||
				Tmpfs("/var/run/nscd", 8192, 0755),
 | 
			
		||||
		},
 | 
			
		||||
		(&bwrap.Config{
 | 
			
		||||
			Net:      true,
 | 
			
		||||
			UserNS:   true,
 | 
			
		||||
			Clearenv: true,
 | 
			
		||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
			
		||||
			Chdir:    "/home/chronos",
 | 
			
		||||
			SetEnv: map[string]string{
 | 
			
		||||
				"HOME":              "/home/chronos",
 | 
			
		||||
				"SHELL":             "/run/current-system/sw/bin/zsh",
 | 
			
		||||
				"TERM":              "xterm-256color",
 | 
			
		||||
				"USER":              "chronos",
 | 
			
		||||
				"XDG_RUNTIME_DIR":   "/run/user/65534",
 | 
			
		||||
				"XDG_SESSION_CLASS": "user",
 | 
			
		||||
				"XDG_SESSION_TYPE":  "tty"},
 | 
			
		||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
			AsInit:        true,
 | 
			
		||||
		}).SetUID(65534).SetGID(65534).
 | 
			
		||||
			Procfs("/proc").
 | 
			
		||||
			Tmpfs(fst.Tmp, 4096).
 | 
			
		||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
			Bind("/bin", "/bin", false, true).
 | 
			
		||||
			Bind("/boot", "/boot", 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("/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("/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").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
			Tmpfs("/run/user", 1048576).
 | 
			
		||||
			Tmpfs("/run/user/65534", 8388608).
 | 
			
		||||
			Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
 | 
			
		||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
			
		||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
			
		||||
			Tmpfs("/var/run/nscd", 8192).
 | 
			
		||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
			
		||||
			Symlink("fortify", "/.fortify/sbin/init0"),
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		"nixos permissive defaults chromium", new(stubNixOS),
 | 
			
		||||
		&fst.Config{
 | 
			
		||||
			ID:   "org.chromium.Chromium",
 | 
			
		||||
			Args: []string{"zsh", "-c", "exec chromium "},
 | 
			
		||||
			ID:      "org.chromium.Chromium",
 | 
			
		||||
			Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
 | 
			
		||||
			Confinement: fst.ConfinementConfig{
 | 
			
		||||
				AppID:    9,
 | 
			
		||||
				Groups:   []string{"video"},
 | 
			
		||||
@ -249,136 +254,141 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			}).
 | 
			
		||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
			
		||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
			
		||||
		&sandbox.Params{
 | 
			
		||||
			Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
			
		||||
			Dir:   "/home/chronos",
 | 
			
		||||
			Path:  "/run/current-system/sw/bin/zsh",
 | 
			
		||||
			Args:  []string{"zsh", "-c", "exec chromium "},
 | 
			
		||||
			Env: []string{
 | 
			
		||||
				"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
 | 
			
		||||
				"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
			
		||||
				"HOME=/home/chronos",
 | 
			
		||||
				"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
			
		||||
				"PULSE_SERVER=unix:/run/user/65534/pulse/native",
 | 
			
		||||
				"TERM=xterm-256color",
 | 
			
		||||
				"USER=chronos",
 | 
			
		||||
				"WAYLAND_DISPLAY=wayland-0",
 | 
			
		||||
				"XDG_RUNTIME_DIR=/run/user/65534",
 | 
			
		||||
				"XDG_SESSION_CLASS=user",
 | 
			
		||||
				"XDG_SESSION_TYPE=tty",
 | 
			
		||||
		(&bwrap.Config{
 | 
			
		||||
			Net:      true,
 | 
			
		||||
			UserNS:   true,
 | 
			
		||||
			Chdir:    "/home/chronos",
 | 
			
		||||
			Clearenv: true,
 | 
			
		||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
			
		||||
			SetEnv: map[string]string{
 | 
			
		||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
			
		||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
			
		||||
				"HOME":                     "/home/chronos",
 | 
			
		||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
			
		||||
				"PULSE_SERVER":             "unix:/run/user/65534/pulse/native",
 | 
			
		||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
			
		||||
				"TERM":                     "xterm-256color",
 | 
			
		||||
				"USER":                     "chronos",
 | 
			
		||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
			
		||||
				"XDG_RUNTIME_DIR":          "/run/user/65534",
 | 
			
		||||
				"XDG_SESSION_CLASS":        "user",
 | 
			
		||||
				"XDG_SESSION_TYPE":         "tty",
 | 
			
		||||
			},
 | 
			
		||||
			Ops: new(sandbox.Ops).
 | 
			
		||||
				Proc("/proc").
 | 
			
		||||
				Tmpfs(fst.Tmp, 4096, 0755).
 | 
			
		||||
				Dev("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
				Bind("/bin", "/bin", sandbox.BindWritable).
 | 
			
		||||
				Bind("/boot", "/boot", sandbox.BindWritable).
 | 
			
		||||
				Bind("/home", "/home", sandbox.BindWritable).
 | 
			
		||||
				Bind("/lib", "/lib", sandbox.BindWritable).
 | 
			
		||||
				Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
			
		||||
				Bind("/nix", "/nix", sandbox.BindWritable).
 | 
			
		||||
				Bind("/root", "/root", sandbox.BindWritable).
 | 
			
		||||
				Bind("/run", "/run", sandbox.BindWritable).
 | 
			
		||||
				Bind("/srv", "/srv", sandbox.BindWritable).
 | 
			
		||||
				Bind("/sys", "/sys", sandbox.BindWritable).
 | 
			
		||||
				Bind("/usr", "/usr", sandbox.BindWritable).
 | 
			
		||||
				Bind("/var", "/var", sandbox.BindWritable).
 | 
			
		||||
				Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
			
		||||
				Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
			
		||||
				Tmpfs("/run/user/1971", 8192, 0755).
 | 
			
		||||
				Tmpfs("/run/dbus", 8192, 0755).
 | 
			
		||||
				Bind("/etc", fst.Tmp+"/etc", 0).
 | 
			
		||||
				Mkdir("/etc", 0700).
 | 
			
		||||
				Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
			
		||||
				Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
				Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
				Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
				Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
				Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
				Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
				Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
				Link("/proc/mounts", "/etc/mtab").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
				Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
				Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
				Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
				Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
				Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
				Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
				Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
				Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
				Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
				Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
				Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
				Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
				Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
				Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
				Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
				Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
				Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
				Tmpfs("/run/user", 4096, 0755).
 | 
			
		||||
				Tmpfs("/run/user/65534", 8388608, 0755).
 | 
			
		||||
				Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
 | 
			
		||||
				Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
			
		||||
				Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
				Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
			
		||||
				Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
 | 
			
		||||
				Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
 | 
			
		||||
				Place(fst.Tmp+"/pulse-cookie", nil).
 | 
			
		||||
				Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
 | 
			
		||||
				Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
			
		||||
				Tmpfs("/var/run/nscd", 8192, 0755),
 | 
			
		||||
		},
 | 
			
		||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
			AsInit:        true,
 | 
			
		||||
		}).SetUID(65534).SetGID(65534).
 | 
			
		||||
			Procfs("/proc").
 | 
			
		||||
			Tmpfs(fst.Tmp, 4096).
 | 
			
		||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
			
		||||
			Bind("/bin", "/bin", false, true).
 | 
			
		||||
			Bind("/boot", "/boot", 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("/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("/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").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
			
		||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
			
		||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
			
		||||
			Tmpfs("/run/user", 1048576).
 | 
			
		||||
			Tmpfs("/run/user/65534", 8388608).
 | 
			
		||||
			Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
 | 
			
		||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
			
		||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
			
		||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
			
		||||
			Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
 | 
			
		||||
			Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
 | 
			
		||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
			
		||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
 | 
			
		||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
			
		||||
			Tmpfs("/var/run/nscd", 8192).
 | 
			
		||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
			
		||||
			Symlink("fortify", "/.fortify/sbin/init0"),
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,8 +17,7 @@ type stubNixOS struct {
 | 
			
		||||
	usernameErr map[string]error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubNixOS) Getuid() int                              { return 1971 }
 | 
			
		||||
func (s *stubNixOS) Getgid() int                              { return 100 }
 | 
			
		||||
func (s *stubNixOS) Geteuid() int                             { return 1971 }
 | 
			
		||||
func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
			
		||||
func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
			
		||||
func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
			
		||||
@ -55,8 +54,10 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch file {
 | 
			
		||||
	case "zsh":
 | 
			
		||||
		return "/run/current-system/sw/bin/zsh", nil
 | 
			
		||||
	case "sudo":
 | 
			
		||||
		return "/run/wrappers/bin/sudo", nil
 | 
			
		||||
	case "machinectl":
 | 
			
		||||
		return "/home/ophestra/.nix-profile/bin/machinectl", nil
 | 
			
		||||
	default:
 | 
			
		||||
		panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -8,19 +8,19 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type sealTestCase struct {
 | 
			
		||||
	name          string
 | 
			
		||||
	os            sys.State
 | 
			
		||||
	config        *fst.Config
 | 
			
		||||
	id            fst.ID
 | 
			
		||||
	wantSys       *system.I
 | 
			
		||||
	wantContainer *sandbox.Params
 | 
			
		||||
	name      string
 | 
			
		||||
	os        sys.State
 | 
			
		||||
	config    *fst.Config
 | 
			
		||||
	id        fst.ID
 | 
			
		||||
	wantSys   *system.I
 | 
			
		||||
	wantBwrap *bwrap.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestApp(t *testing.T) {
 | 
			
		||||
@ -30,15 +30,15 @@ func TestApp(t *testing.T) {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			a := app.NewWithID(tc.id, tc.os)
 | 
			
		||||
			var (
 | 
			
		||||
				gotSys       *system.I
 | 
			
		||||
				gotContainer *sandbox.Params
 | 
			
		||||
				gotSys   *system.I
 | 
			
		||||
				gotBwrap *bwrap.Config
 | 
			
		||||
			)
 | 
			
		||||
			if !t.Run("seal", func(t *testing.T) {
 | 
			
		||||
				if sa, err := a.Seal(tc.config); err != nil {
 | 
			
		||||
					t.Errorf("Seal: error = %v", err)
 | 
			
		||||
					return
 | 
			
		||||
				} else {
 | 
			
		||||
					gotSys, gotContainer = app.AppIParams(a, sa)
 | 
			
		||||
					gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
 | 
			
		||||
				}
 | 
			
		||||
			}) {
 | 
			
		||||
				return
 | 
			
		||||
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			t.Run("compare params", func(t *testing.T) {
 | 
			
		||||
				if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
 | 
			
		||||
					t.Errorf("seal: params =\n%s\n, want\n%s",
 | 
			
		||||
						mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
 | 
			
		||||
			t.Run("compare bwrap", func(t *testing.T) {
 | 
			
		||||
				if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
 | 
			
		||||
					t.Errorf("seal: bwrap =\n%s\n, want\n%s",
 | 
			
		||||
						mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@ package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
 | 
			
		||||
	return a
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
 | 
			
		||||
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
 | 
			
		||||
	v := a.(*app)
 | 
			
		||||
	seal := sa.(*outcome)
 | 
			
		||||
	if v.outcome != seal || v.id != seal.id {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								internal/app/init0/early.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								internal/app/init0/early.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
package init0
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// used by the parent process
 | 
			
		||||
 | 
			
		||||
// TryArgv0 calls [Main] if the last element of argv0 is "init0".
 | 
			
		||||
func TryArgv0() {
 | 
			
		||||
	if len(os.Args) > 0 && path.Base(os.Args[0]) == "init0" {
 | 
			
		||||
		Main()
 | 
			
		||||
		internal.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								internal/app/init0/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								internal/app/init0/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,165 @@
 | 
			
		||||
package init0
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// time to wait for linger processes after death of initial process
 | 
			
		||||
	residualProcessTimeout = 5 * time.Second
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// everything beyond this point runs within pid namespace
 | 
			
		||||
// proceed with caution!
 | 
			
		||||
 | 
			
		||||
func Main() {
 | 
			
		||||
	// sharing stdout with shim
 | 
			
		||||
	// USE WITH CAUTION
 | 
			
		||||
	fmsg.Prepare("init0")
 | 
			
		||||
 | 
			
		||||
	// setting this prevents ptrace
 | 
			
		||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
			
		||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if os.Getpid() != 1 {
 | 
			
		||||
		log.Fatal("this process must run as pid 1")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// receive setup payload
 | 
			
		||||
	var (
 | 
			
		||||
		payload    Payload
 | 
			
		||||
		closeSetup func() error
 | 
			
		||||
	)
 | 
			
		||||
	if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
 | 
			
		||||
		if errors.Is(err, sandbox.ErrInvalid) {
 | 
			
		||||
			log.Fatal("invalid config descriptor")
 | 
			
		||||
		}
 | 
			
		||||
		if errors.Is(err, sandbox.ErrNotSet) {
 | 
			
		||||
			log.Fatal("FORTIFY_INIT not set")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Fatalf("cannot decode init setup payload: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		fmsg.Store(payload.Verbose)
 | 
			
		||||
		closeSetup = f
 | 
			
		||||
 | 
			
		||||
		// child does not need to see this
 | 
			
		||||
		if err = os.Unsetenv(Env); err != nil {
 | 
			
		||||
			log.Printf("cannot unset %s: %v", Env, err)
 | 
			
		||||
			// not fatal
 | 
			
		||||
		} else {
 | 
			
		||||
			fmsg.Verbose("received configuration")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// die with parent
 | 
			
		||||
	if err := sandbox.SetPdeathsig(syscall.SIGKILL); err != nil {
 | 
			
		||||
		log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command(payload.Argv0)
 | 
			
		||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
	cmd.Args = payload.Argv
 | 
			
		||||
	cmd.Env = os.Environ()
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Start(); err != nil {
 | 
			
		||||
		log.Fatalf("cannot start %q: %v", payload.Argv0, err)
 | 
			
		||||
	}
 | 
			
		||||
	fmsg.Suspend()
 | 
			
		||||
 | 
			
		||||
	// close setup pipe as setup is now complete
 | 
			
		||||
	if err := closeSetup(); err != nil {
 | 
			
		||||
		log.Println("cannot close setup pipe:", err)
 | 
			
		||||
		// not fatal
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sig := make(chan os.Signal, 2)
 | 
			
		||||
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
 | 
			
		||||
	type winfo struct {
 | 
			
		||||
		wpid    int
 | 
			
		||||
		wstatus syscall.WaitStatus
 | 
			
		||||
	}
 | 
			
		||||
	info := make(chan winfo, 1)
 | 
			
		||||
	done := make(chan struct{})
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		var (
 | 
			
		||||
			err     error
 | 
			
		||||
			wpid    = -2
 | 
			
		||||
			wstatus syscall.WaitStatus
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		// keep going until no child process is left
 | 
			
		||||
		for wpid != -1 {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if wpid != -2 {
 | 
			
		||||
				info <- winfo{wpid, wstatus}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = syscall.EINTR
 | 
			
		||||
			for errors.Is(err, syscall.EINTR) {
 | 
			
		||||
				wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !errors.Is(err, syscall.ECHILD) {
 | 
			
		||||
			log.Println("unexpected wait4 response:", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		close(done)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// closed after residualProcessTimeout has elapsed after initial process death
 | 
			
		||||
	timeout := make(chan struct{})
 | 
			
		||||
 | 
			
		||||
	r := 2
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case s := <-sig:
 | 
			
		||||
			if fmsg.Resume() {
 | 
			
		||||
				fmsg.Verbosef("terminating on %s after process start", s.String())
 | 
			
		||||
			} else {
 | 
			
		||||
				fmsg.Verbosef("terminating on %s", s.String())
 | 
			
		||||
			}
 | 
			
		||||
			internal.Exit(0)
 | 
			
		||||
		case w := <-info:
 | 
			
		||||
			if w.wpid == cmd.Process.Pid {
 | 
			
		||||
				// initial process exited, output is most likely available again
 | 
			
		||||
				fmsg.Resume()
 | 
			
		||||
 | 
			
		||||
				switch {
 | 
			
		||||
				case w.wstatus.Exited():
 | 
			
		||||
					r = w.wstatus.ExitStatus()
 | 
			
		||||
				case w.wstatus.Signaled():
 | 
			
		||||
					r = 128 + int(w.wstatus.Signal())
 | 
			
		||||
				default:
 | 
			
		||||
					r = 255
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				go func() {
 | 
			
		||||
					time.Sleep(residualProcessTimeout)
 | 
			
		||||
					close(timeout)
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
		case <-done:
 | 
			
		||||
			internal.Exit(r)
 | 
			
		||||
		case <-timeout:
 | 
			
		||||
			log.Println("timeout exceeded waiting for lingering processes")
 | 
			
		||||
			internal.Exit(r)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								internal/app/init0/payload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								internal/app/init0/payload.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
package init0
 | 
			
		||||
 | 
			
		||||
const Env = "FORTIFY_INIT"
 | 
			
		||||
 | 
			
		||||
type Payload struct {
 | 
			
		||||
	// target full exec path
 | 
			
		||||
	Argv0 string
 | 
			
		||||
	// child full argv
 | 
			
		||||
	Argv []string
 | 
			
		||||
 | 
			
		||||
	// verbosity pass through
 | 
			
		||||
	Verbose bool
 | 
			
		||||
}
 | 
			
		||||
@ -3,12 +3,15 @@ package app
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
@ -18,7 +21,7 @@ import (
 | 
			
		||||
 | 
			
		||||
const shimSetupTimeout = 5 * time.Second
 | 
			
		||||
 | 
			
		||||
func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
			
		||||
	if !seal.f.CompareAndSwap(false, true) {
 | 
			
		||||
		// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
			
		||||
		// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
			
		||||
@ -34,11 +37,33 @@ func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
	fmsg.Verbosef("version %s", internal.Version())
 | 
			
		||||
	fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		resolve exec paths
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	shimExec := [2]string{helper.BubblewrapName}
 | 
			
		||||
	if len(seal.command) > 0 {
 | 
			
		||||
		shimExec[1] = seal.command[0]
 | 
			
		||||
	}
 | 
			
		||||
	for i, n := range shimExec {
 | 
			
		||||
		if len(n) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if filepath.Base(n) == n {
 | 
			
		||||
			if s, err := exec.LookPath(n); err == nil {
 | 
			
		||||
				shimExec[i] = s
 | 
			
		||||
			} else {
 | 
			
		||||
				return fmsg.WrapError(err,
 | 
			
		||||
					fmt.Sprintf("executable file %q not found in $PATH", n))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		prepare/revert os state
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	if err := seal.sys.Commit(seal.ctx); err != nil {
 | 
			
		||||
	if err := seal.sys.Commit(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	store := state.NewMulti(seal.runDirPath)
 | 
			
		||||
@ -112,6 +137,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
	if startTime, err := cmd.Start(
 | 
			
		||||
		seal.user.aid.String(),
 | 
			
		||||
		seal.user.supp,
 | 
			
		||||
		seal.bwrapSync,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
@ -119,7 +145,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
		rs.Time = startTime
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
 | 
			
		||||
	c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
@ -128,9 +154,11 @@ func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
		cancel()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Serve(ctx, &shim.Params{
 | 
			
		||||
		Container: seal.container,
 | 
			
		||||
		Home:      seal.user.data,
 | 
			
		||||
	if err := cmd.Serve(c, &shim.Payload{
 | 
			
		||||
		Argv:  seal.command,
 | 
			
		||||
		Exec:  shimExec,
 | 
			
		||||
		Bwrap: seal.container,
 | 
			
		||||
		Home:  seal.user.data,
 | 
			
		||||
 | 
			
		||||
		Verbose: fmsg.Load(),
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
@ -171,22 +199,18 @@ func (seal *outcome) Run(rs *fst.RunState) error {
 | 
			
		||||
	// this is reached when a fault makes an already running shim impossible to continue execution
 | 
			
		||||
	// however a kill signal could not be delivered (should actually always happen like that since fsu)
 | 
			
		||||
	// the effects of this is similar to the alternative exit path and ensures shim death
 | 
			
		||||
	case err := <-cmd.Fallback():
 | 
			
		||||
	case err := <-cmd.WaitFallback():
 | 
			
		||||
		rs.ExitCode = 255
 | 
			
		||||
		log.Printf("cannot terminate shim on faulted setup: %v", err)
 | 
			
		||||
 | 
			
		||||
	// alternative exit path relying on shim behaviour on monitor process exit
 | 
			
		||||
	case <-seal.ctx.Done():
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		fmsg.Verbose("alternative exit path selected")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmsg.Resume()
 | 
			
		||||
	if seal.sync != nil {
 | 
			
		||||
		if err := seal.sync.Close(); err != nil {
 | 
			
		||||
			log.Printf("cannot close wayland security context: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if seal.dbusMsg != nil {
 | 
			
		||||
		// dump dbus message buffer
 | 
			
		||||
		seal.dbusMsg()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,28 +2,24 @@ package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"maps"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/system"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/wl"
 | 
			
		||||
)
 | 
			
		||||
@ -69,19 +65,19 @@ type outcome struct {
 | 
			
		||||
	// copied from [sys.State] response
 | 
			
		||||
	runDirPath string
 | 
			
		||||
 | 
			
		||||
	// passed through from [fst.Config]
 | 
			
		||||
	command []string
 | 
			
		||||
 | 
			
		||||
	// initial [fst.Config] gob stream for state data;
 | 
			
		||||
	// this is prepared ahead of time as config is clobbered during seal creation
 | 
			
		||||
	// this is prepared ahead of time as config is mutated during seal creation
 | 
			
		||||
	ct io.WriterTo
 | 
			
		||||
	// dump dbus proxy message buffer
 | 
			
		||||
	dbusMsg func()
 | 
			
		||||
 | 
			
		||||
	user fsuUser
 | 
			
		||||
	sys  *system.I
 | 
			
		||||
	ctx  context.Context
 | 
			
		||||
 | 
			
		||||
	container *sandbox.Params
 | 
			
		||||
	env       map[string]string
 | 
			
		||||
	sync      *os.File
 | 
			
		||||
	user      fsuUser
 | 
			
		||||
	sys       *system.I
 | 
			
		||||
	container *bwrap.Config
 | 
			
		||||
	bwrapSync *os.File
 | 
			
		||||
 | 
			
		||||
	f atomic.Bool
 | 
			
		||||
}
 | 
			
		||||
@ -104,17 +100,7 @@ type fsuUser struct {
 | 
			
		||||
	username string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
 | 
			
		||||
	if seal.ctx != nil {
 | 
			
		||||
		panic("finalise called twice")
 | 
			
		||||
	}
 | 
			
		||||
	seal.ctx = ctx
 | 
			
		||||
 | 
			
		||||
	shellPath := "/bin/sh"
 | 
			
		||||
	if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
 | 
			
		||||
		shellPath = s
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
			
		||||
	{
 | 
			
		||||
		// encode initial configuration for state tracking
 | 
			
		||||
		ct := new(bytes.Buffer)
 | 
			
		||||
@ -125,6 +111,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		seal.ct = ct
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// pass through command slice; this value is never touched in the main process
 | 
			
		||||
	seal.command = config.Command
 | 
			
		||||
 | 
			
		||||
	// allowed aid range 0 to 9999, this is checked again in fsu
 | 
			
		||||
	if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
			
		||||
		return fmsg.WrapError(ErrUser,
 | 
			
		||||
@ -178,24 +167,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
	if config.Confinement.Sandbox == nil {
 | 
			
		||||
		fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
			
		||||
 | 
			
		||||
		// fsu clears the environment so resolve paths early
 | 
			
		||||
		if !path.IsAbs(config.Path) {
 | 
			
		||||
			if len(config.Args) > 0 {
 | 
			
		||||
				if p, err := sys.LookPath(config.Args[0]); err != nil {
 | 
			
		||||
					return fmsg.WrapError(err, err.Error())
 | 
			
		||||
				} else {
 | 
			
		||||
					config.Path = p
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				config.Path = shellPath
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		conf := &fst.SandboxConfig{
 | 
			
		||||
			Userns:  true,
 | 
			
		||||
			Net:     true,
 | 
			
		||||
			Tty:     true,
 | 
			
		||||
			AutoEtc: true,
 | 
			
		||||
			UserNS:       true,
 | 
			
		||||
			Net:          true,
 | 
			
		||||
			Syscall:      new(bwrap.SyscallPolicy),
 | 
			
		||||
			NoNewSession: true,
 | 
			
		||||
			AutoEtc:      true,
 | 
			
		||||
		}
 | 
			
		||||
		// bind entries in /
 | 
			
		||||
		if d, err := sys.ReadDir("/"); err != nil {
 | 
			
		||||
@ -221,7 +198,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		// hide nscd from sandbox if present
 | 
			
		||||
		nscd := "/var/run/nscd"
 | 
			
		||||
		if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
			
		||||
			conf.Cover = append(conf.Cover, nscd)
 | 
			
		||||
			conf.Override = append(conf.Override, nscd)
 | 
			
		||||
		}
 | 
			
		||||
		// bind GPU stuff
 | 
			
		||||
		if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
 | 
			
		||||
@ -233,29 +210,17 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		config.Confinement.Sandbox = conf
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var mapuid, mapgid *stringPair[int]
 | 
			
		||||
	var mapuid *stringPair[int]
 | 
			
		||||
	{
 | 
			
		||||
		var uid, gid int
 | 
			
		||||
		var uid int
 | 
			
		||||
		var err error
 | 
			
		||||
		seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
 | 
			
		||||
		seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmsg.WrapErrorSuffix(err,
 | 
			
		||||
				"cannot initialise container configuration:")
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if !path.IsAbs(config.Path) {
 | 
			
		||||
			return fmsg.WrapError(syscall.EINVAL,
 | 
			
		||||
				"invalid program path")
 | 
			
		||||
		}
 | 
			
		||||
		if len(config.Args) == 0 {
 | 
			
		||||
			config.Args = []string{config.Path}
 | 
			
		||||
		}
 | 
			
		||||
		seal.container.Path = config.Path
 | 
			
		||||
		seal.container.Args = config.Args
 | 
			
		||||
 | 
			
		||||
		mapuid = newInt(uid)
 | 
			
		||||
		mapgid = newInt(gid)
 | 
			
		||||
		if seal.env == nil {
 | 
			
		||||
			seal.env = make(map[string]string)
 | 
			
		||||
		if seal.container.SetEnv == nil {
 | 
			
		||||
			seal.container.SetEnv = make(map[string]string)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -290,27 +255,35 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
 | 
			
		||||
	// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
			
		||||
	innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
			
		||||
	seal.container.Tmpfs("/run/user", 1<<12, 0755)
 | 
			
		||||
	seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755)
 | 
			
		||||
	seal.env[xdgRuntimeDir] = innerRuntimeDir
 | 
			
		||||
	seal.env[xdgSessionClass] = "user"
 | 
			
		||||
	seal.env[xdgSessionType] = "tty"
 | 
			
		||||
	seal.container.Tmpfs("/run/user", 1*1024*1024)
 | 
			
		||||
	seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
 | 
			
		||||
	seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
 | 
			
		||||
	seal.container.SetEnv[xdgSessionClass] = "user"
 | 
			
		||||
	seal.container.SetEnv[xdgSessionType] = "tty"
 | 
			
		||||
 | 
			
		||||
	// outer path for inner /tmp
 | 
			
		||||
	{
 | 
			
		||||
		tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
			
		||||
		seal.sys.Ensure(tmpdir, 0700)
 | 
			
		||||
		seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
			
		||||
		tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
 | 
			
		||||
		seal.sys.Ensure(tmpdirInst, 01700)
 | 
			
		||||
		seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
 | 
			
		||||
		seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
 | 
			
		||||
		tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
 | 
			
		||||
		seal.sys.Ensure(tmpdirProc, 01700)
 | 
			
		||||
		seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
 | 
			
		||||
		seal.container.Bind(tmpdirProc, "/tmp", false, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		Passwd database
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	// look up shell
 | 
			
		||||
	sh := "/bin/sh"
 | 
			
		||||
	if s, ok := sys.LookupEnv(shell); ok {
 | 
			
		||||
		seal.container.SetEnv[shell] = s
 | 
			
		||||
		sh = s
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// bind home directory
 | 
			
		||||
	homeDir := "/var/empty"
 | 
			
		||||
	if seal.user.home != "" {
 | 
			
		||||
		homeDir = seal.user.home
 | 
			
		||||
@ -319,25 +292,27 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
	if seal.user.username != "" {
 | 
			
		||||
		username = seal.user.username
 | 
			
		||||
	}
 | 
			
		||||
	seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
 | 
			
		||||
	seal.container.Dir = homeDir
 | 
			
		||||
	seal.env["HOME"] = homeDir
 | 
			
		||||
	seal.env["USER"] = username
 | 
			
		||||
	seal.container.Bind(seal.user.data, homeDir, false, true)
 | 
			
		||||
	seal.container.Chdir = homeDir
 | 
			
		||||
	seal.container.SetEnv["HOME"] = homeDir
 | 
			
		||||
	seal.container.SetEnv["USER"] = username
 | 
			
		||||
 | 
			
		||||
	seal.container.Place("/etc/passwd",
 | 
			
		||||
		[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
 | 
			
		||||
	seal.container.Place("/etc/group",
 | 
			
		||||
		[]byte("fortify:x:"+mapgid.String()+":\n"))
 | 
			
		||||
	// generate /etc/passwd and /etc/group
 | 
			
		||||
	seal.container.CopyBind("/etc/passwd",
 | 
			
		||||
		[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
 | 
			
		||||
	seal.container.CopyBind("/etc/group",
 | 
			
		||||
		[]byte("fortify:x:"+mapuid.String()+":\n"))
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		Display servers
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	// pass $TERM for proper terminal I/O in shell
 | 
			
		||||
	// pass $TERM to launcher
 | 
			
		||||
	if t, ok := sys.LookupEnv(term); ok {
 | 
			
		||||
		seal.env[term] = t
 | 
			
		||||
		seal.container.SetEnv[term] = t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set up wayland
 | 
			
		||||
	if config.Confinement.Enablements.Has(system.EWayland) {
 | 
			
		||||
		// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
			
		||||
		var socketPath string
 | 
			
		||||
@ -351,7 +326,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
			
		||||
		seal.env[wl.WaylandDisplay] = wl.FallbackName
 | 
			
		||||
		seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
 | 
			
		||||
 | 
			
		||||
		if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
			
		||||
			socketDir := path.Join(sc.SharePath, "wayland")
 | 
			
		||||
@ -362,23 +337,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
				// use instance ID in case app id is not set
 | 
			
		||||
				appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
			
		||||
			}
 | 
			
		||||
			seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
 | 
			
		||||
			seal.container.Bind(outerPath, innerPath, 0)
 | 
			
		||||
			seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
 | 
			
		||||
			seal.container.Bind(outerPath, innerPath)
 | 
			
		||||
		} else { // bind mount wayland socket (insecure)
 | 
			
		||||
			fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
			
		||||
			seal.container.Bind(socketPath, innerPath, 0)
 | 
			
		||||
			seal.container.Bind(socketPath, innerPath)
 | 
			
		||||
			seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set up X11
 | 
			
		||||
	if config.Confinement.Enablements.Has(system.EX11) {
 | 
			
		||||
		// discover X11 and grant user permission via the `ChangeHosts` command
 | 
			
		||||
		if d, ok := sys.LookupEnv(display); !ok {
 | 
			
		||||
			return fmsg.WrapError(ErrXDisplay,
 | 
			
		||||
				"DISPLAY is not set")
 | 
			
		||||
		} else {
 | 
			
		||||
			seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
			
		||||
			seal.env[display] = d
 | 
			
		||||
			seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
 | 
			
		||||
			seal.container.SetEnv[display] = d
 | 
			
		||||
			seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -419,8 +396,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
			
		||||
		innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
			
		||||
		seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
			
		||||
		seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
 | 
			
		||||
		seal.env[pulseServer] = "unix:" + innerPulseSocket
 | 
			
		||||
		seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
 | 
			
		||||
		seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
 | 
			
		||||
 | 
			
		||||
		// publish current user's pulse cookie for target user
 | 
			
		||||
		if src, err := discoverPulseCookie(sys); err != nil {
 | 
			
		||||
@ -428,9 +405,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
			fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
			
		||||
		} else {
 | 
			
		||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
			
		||||
			seal.env[pulseCookie] = innerDst
 | 
			
		||||
			var payload *[]byte
 | 
			
		||||
			seal.container.PlaceP(innerDst, &payload)
 | 
			
		||||
			seal.container.SetEnv[pulseCookie] = innerDst
 | 
			
		||||
			payload := new([]byte)
 | 
			
		||||
			seal.container.CopyBindRef(innerDst, &payload)
 | 
			
		||||
			seal.sys.CopyFile(payload, src, 256, 256)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -460,13 +437,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
 | 
			
		||||
		// share proxy sockets
 | 
			
		||||
		sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
			
		||||
		seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
			
		||||
		seal.container.Bind(sessionPath, sessionInner, 0)
 | 
			
		||||
		seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
			
		||||
		seal.container.Bind(sessionPath, sessionInner)
 | 
			
		||||
		seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
			
		||||
		if config.Confinement.SystemBus != nil {
 | 
			
		||||
			systemInner := "/run/dbus/system_bus_socket"
 | 
			
		||||
			seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
			
		||||
			seal.container.Bind(systemPath, systemInner, 0)
 | 
			
		||||
			seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
			
		||||
			seal.container.Bind(systemPath, systemInner)
 | 
			
		||||
			seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -475,8 +452,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		Miscellaneous
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	for _, dest := range config.Confinement.Sandbox.Cover {
 | 
			
		||||
		seal.container.Tmpfs(dest, 1<<13, 0755)
 | 
			
		||||
	// queue overriding tmpfs at the end of seal.container.Filesystem
 | 
			
		||||
	for _, dest := range config.Confinement.Sandbox.Override {
 | 
			
		||||
		seal.container.Tmpfs(dest, 8*1024)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// append ExtraPerms last
 | 
			
		||||
@ -502,13 +480,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
 | 
			
		||||
		seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// flatten and sort env for deterministic behaviour
 | 
			
		||||
	seal.container.Env = make([]string, 0, len(seal.env))
 | 
			
		||||
	maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
 | 
			
		||||
	slices.Sort(seal.container.Env)
 | 
			
		||||
	// mount fortify in sandbox for init
 | 
			
		||||
	seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
 | 
			
		||||
	seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init0"))
 | 
			
		||||
 | 
			
		||||
	fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
 | 
			
		||||
		seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
 | 
			
		||||
	fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
 | 
			
		||||
		seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,26 +7,18 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/init0"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Env = "FORTIFY_SHIM"
 | 
			
		||||
 | 
			
		||||
type Params struct {
 | 
			
		||||
	// finalised container params
 | 
			
		||||
	Container *sandbox.Params
 | 
			
		||||
	// path to outer home directory
 | 
			
		||||
	Home string
 | 
			
		||||
 | 
			
		||||
	// verbosity pass through
 | 
			
		||||
	Verbose bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// everything beyond this point runs as unconstrained target user
 | 
			
		||||
// proceed with caution!
 | 
			
		||||
 | 
			
		||||
@ -35,15 +27,17 @@ func Main() {
 | 
			
		||||
	// USE WITH CAUTION
 | 
			
		||||
	fmsg.Prepare("shim")
 | 
			
		||||
 | 
			
		||||
	// setting this prevents ptrace
 | 
			
		||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
			
		||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// receive setup payload
 | 
			
		||||
	var (
 | 
			
		||||
		params     Params
 | 
			
		||||
		payload    Payload
 | 
			
		||||
		closeSetup func() error
 | 
			
		||||
	)
 | 
			
		||||
	if f, err := sandbox.Receive(Env, ¶ms, nil); err != nil {
 | 
			
		||||
	if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
 | 
			
		||||
		if errors.Is(err, sandbox.ErrInvalid) {
 | 
			
		||||
			log.Fatal("invalid config descriptor")
 | 
			
		||||
		}
 | 
			
		||||
@ -51,26 +45,32 @@ func Main() {
 | 
			
		||||
			log.Fatal("FORTIFY_SHIM not set")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Fatalf("cannot receive shim setup params: %v", err)
 | 
			
		||||
		log.Fatalf("cannot decode shim setup payload: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		internal.InstallFmsg(params.Verbose)
 | 
			
		||||
		internal.InstallFmsg(payload.Verbose)
 | 
			
		||||
		closeSetup = f
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if params.Container == nil || params.Container.Ops == nil {
 | 
			
		||||
		log.Fatal("invalid container params")
 | 
			
		||||
	if payload.Bwrap == nil {
 | 
			
		||||
		log.Fatal("bwrap config not supplied")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// restore bwrap sync fd
 | 
			
		||||
	var syncFd *os.File
 | 
			
		||||
	if payload.Sync != nil {
 | 
			
		||||
		syncFd = os.NewFile(*payload.Sync, "sync")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// close setup socket
 | 
			
		||||
	if err := closeSetup(); err != nil {
 | 
			
		||||
		log.Printf("cannot close setup pipe: %v", err)
 | 
			
		||||
		log.Println("cannot close setup pipe:", err)
 | 
			
		||||
		// not fatal
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ensure home directory as target user
 | 
			
		||||
	if s, err := os.Stat(params.Home); err != nil {
 | 
			
		||||
	if s, err := os.Stat(payload.Home); err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			if err = os.Mkdir(params.Home, 0700); err != nil {
 | 
			
		||||
			if err = os.Mkdir(payload.Home, 0700); err != nil {
 | 
			
		||||
				log.Fatalf("cannot create home directory: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
@ -79,37 +79,72 @@ func Main() {
 | 
			
		||||
 | 
			
		||||
		// home directory is created, proceed
 | 
			
		||||
	} else if !s.IsDir() {
 | 
			
		||||
		log.Fatalf("path %q is not a directory", params.Home)
 | 
			
		||||
		log.Fatalf("data path %q is not a directory", payload.Home)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var name string
 | 
			
		||||
	if len(params.Container.Args) > 0 {
 | 
			
		||||
		name = params.Container.Args[0]
 | 
			
		||||
	var ic init0.Payload
 | 
			
		||||
 | 
			
		||||
	// resolve argv0
 | 
			
		||||
	ic.Argv = payload.Argv
 | 
			
		||||
	if len(ic.Argv) > 0 {
 | 
			
		||||
		// looked up from $PATH by parent
 | 
			
		||||
		ic.Argv0 = payload.Exec[1]
 | 
			
		||||
	} else {
 | 
			
		||||
		// no argv, look up shell instead
 | 
			
		||||
		var ok bool
 | 
			
		||||
		if payload.Bwrap.SetEnv == nil {
 | 
			
		||||
			log.Fatal("no command was specified and environment is unset")
 | 
			
		||||
		}
 | 
			
		||||
		if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
 | 
			
		||||
			log.Fatal("no command was specified and $SHELL was unset")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ic.Argv = []string{ic.Argv0}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf := payload.Bwrap
 | 
			
		||||
 | 
			
		||||
	var extraFiles []*os.File
 | 
			
		||||
 | 
			
		||||
	// serve setup payload
 | 
			
		||||
	if fd, encoder, err := sandbox.Setup(&extraFiles); err != nil {
 | 
			
		||||
		log.Fatalf("cannot pipe: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		conf.SetEnv[init0.Env] = strconv.Itoa(fd)
 | 
			
		||||
		go func() {
 | 
			
		||||
			fmsg.Verbose("transmitting config to init")
 | 
			
		||||
			if err = encoder.Encode(&ic); err != nil {
 | 
			
		||||
				log.Fatalf("cannot transmit init config: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
			
		||||
 | 
			
		||||
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
	defer stop() // unreachable
 | 
			
		||||
	container := sandbox.New(ctx, name)
 | 
			
		||||
	container.Params = *params.Container
 | 
			
		||||
	container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
	container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
 | 
			
		||||
	container.WaitDelay = 2 * time.Second
 | 
			
		||||
 | 
			
		||||
	if err := container.Start(); err != nil {
 | 
			
		||||
		fmsg.PrintBaseError(err, "cannot start container:")
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	if err := container.Serve(); err != nil {
 | 
			
		||||
		fmsg.PrintBaseError(err, "cannot configure container:")
 | 
			
		||||
	}
 | 
			
		||||
	if err := container.Wait(); err != nil {
 | 
			
		||||
		var exitError *exec.ExitError
 | 
			
		||||
		if !errors.As(err, &exitError) {
 | 
			
		||||
			if errors.Is(err, context.Canceled) {
 | 
			
		||||
				os.Exit(2)
 | 
			
		||||
	if b, err := helper.NewBwrap(
 | 
			
		||||
		ctx, path.Join(fst.Tmp, "sbin/init0"),
 | 
			
		||||
		nil, false,
 | 
			
		||||
		func(int, int) []string { return make([]string, 0) },
 | 
			
		||||
		func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
 | 
			
		||||
		extraFiles,
 | 
			
		||||
		conf, syncFd,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		log.Fatalf("malformed sandbox config: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		// run and pass through exit code
 | 
			
		||||
		if err = b.Start(); err != nil {
 | 
			
		||||
			log.Fatalf("cannot start target process: %v", err)
 | 
			
		||||
		} else if err = b.Wait(); err != nil {
 | 
			
		||||
			var exitError *exec.ExitError
 | 
			
		||||
			if !errors.As(err, &exitError) {
 | 
			
		||||
				log.Printf("wait: %v", err)
 | 
			
		||||
				internal.Exit(127)
 | 
			
		||||
				panic("unreachable")
 | 
			
		||||
			}
 | 
			
		||||
			log.Printf("wait: %v", err)
 | 
			
		||||
			os.Exit(127)
 | 
			
		||||
			internal.Exit(exitError.ExitCode())
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}
 | 
			
		||||
		os.Exit(exitError.ExitCode())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,9 @@ import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
@ -25,11 +25,10 @@ type Shim struct {
 | 
			
		||||
	killFallback chan error
 | 
			
		||||
	// monitor to shim encoder
 | 
			
		||||
	encoder *gob.Encoder
 | 
			
		||||
	// bwrap --sync-fd value
 | 
			
		||||
	sync *uintptr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) Unwrap() *exec.Cmd    { return s.cmd }
 | 
			
		||||
func (s *Shim) Fallback() chan error { return s.killFallback }
 | 
			
		||||
 | 
			
		||||
func (s *Shim) String() string {
 | 
			
		||||
	if s.cmd == nil {
 | 
			
		||||
		return "(unused shim manager)"
 | 
			
		||||
@ -37,9 +36,21 @@ func (s *Shim) String() string {
 | 
			
		||||
	return s.cmd.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) Unwrap() *exec.Cmd {
 | 
			
		||||
	return s.cmd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) WaitFallback() chan error {
 | 
			
		||||
	return s.killFallback
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) Start(
 | 
			
		||||
	// string representation of application id
 | 
			
		||||
	aid string,
 | 
			
		||||
	// string representation of supplementary group ids
 | 
			
		||||
	supp []string,
 | 
			
		||||
	// bwrap --sync-fd
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
) (*time.Time, error) {
 | 
			
		||||
	// prepare user switcher invocation
 | 
			
		||||
	fsuPath := internal.MustFsuPath()
 | 
			
		||||
@ -65,6 +76,12 @@ func (s *Shim) Start(
 | 
			
		||||
	s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
	s.cmd.Dir = "/"
 | 
			
		||||
 | 
			
		||||
	// pass sync fd if set
 | 
			
		||||
	if syncFd != nil {
 | 
			
		||||
		fd := proc.ExtraFile(s.cmd, syncFd)
 | 
			
		||||
		s.sync = &fd
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
			
		||||
	// withhold messages to stderr
 | 
			
		||||
	fmsg.Suspend()
 | 
			
		||||
@ -73,11 +90,10 @@ func (s *Shim) Start(
 | 
			
		||||
			"cannot start fsu:")
 | 
			
		||||
	}
 | 
			
		||||
	startTime := time.Now().UTC()
 | 
			
		||||
 | 
			
		||||
	return &startTime, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) Serve(ctx context.Context, params *Params) error {
 | 
			
		||||
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
			
		||||
	// kill shim if something goes wrong and an error is returned
 | 
			
		||||
	s.killFallback = make(chan error, 1)
 | 
			
		||||
	killShim := func() {
 | 
			
		||||
@ -87,8 +103,9 @@ func (s *Shim) Serve(ctx context.Context, params *Params) error {
 | 
			
		||||
	}
 | 
			
		||||
	defer func() { killShim() }()
 | 
			
		||||
 | 
			
		||||
	payload.Sync = s.sync
 | 
			
		||||
	encodeErr := make(chan error)
 | 
			
		||||
	go func() { encodeErr <- s.encoder.Encode(params) }()
 | 
			
		||||
	go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	// encode return indicates setup completion
 | 
			
		||||
@ -104,11 +121,11 @@ func (s *Shim) Serve(ctx context.Context, params *Params) error {
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		err := ctx.Err()
 | 
			
		||||
		if errors.Is(err, context.Canceled) {
 | 
			
		||||
			return fmsg.WrapError(syscall.ECANCELED,
 | 
			
		||||
			return fmsg.WrapError(errors.New("shim setup canceled"),
 | 
			
		||||
				"shim setup canceled")
 | 
			
		||||
		}
 | 
			
		||||
		if errors.Is(err, context.DeadlineExceeded) {
 | 
			
		||||
			return fmsg.WrapError(syscall.ETIMEDOUT,
 | 
			
		||||
			return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
 | 
			
		||||
				"deadline exceeded waiting for shim")
 | 
			
		||||
		}
 | 
			
		||||
		// unreachable
 | 
			
		||||
							
								
								
									
										23
									
								
								internal/app/shim/payload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								internal/app/shim/payload.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
package shim
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Env = "FORTIFY_SHIM"
 | 
			
		||||
 | 
			
		||||
type Payload struct {
 | 
			
		||||
	// child full argv
 | 
			
		||||
	Argv []string
 | 
			
		||||
	// bwrap, target full exec path
 | 
			
		||||
	Exec [2]string
 | 
			
		||||
	// bwrap config
 | 
			
		||||
	Bwrap *bwrap.Config
 | 
			
		||||
	// path to outer home directory
 | 
			
		||||
	Home string
 | 
			
		||||
	// sync fd
 | 
			
		||||
	Sync *uintptr
 | 
			
		||||
 | 
			
		||||
	// verbosity pass through
 | 
			
		||||
	Verbose bool
 | 
			
		||||
}
 | 
			
		||||
@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
		} else {
 | 
			
		||||
			slices.Sort(aids)
 | 
			
		||||
			want := []int{0, 1}
 | 
			
		||||
			if !slices.Equal(aids, want) {
 | 
			
		||||
			if slices.Compare(aids, want) != 0 {
 | 
			
		||||
				t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -12,10 +12,8 @@ import (
 | 
			
		||||
 | 
			
		||||
// State provides safe interaction with operating system state.
 | 
			
		||||
type State interface {
 | 
			
		||||
	// Getuid provides [os.Getuid].
 | 
			
		||||
	Getuid() int
 | 
			
		||||
	// Getgid provides [os.Getgid].
 | 
			
		||||
	Getgid() int
 | 
			
		||||
	// Geteuid provides [os.Geteuid].
 | 
			
		||||
	Geteuid() int
 | 
			
		||||
	// LookupEnv provides [os.LookupEnv].
 | 
			
		||||
	LookupEnv(key string) (string, bool)
 | 
			
		||||
	// TempDir provides [os.TempDir].
 | 
			
		||||
@ -49,7 +47,7 @@ type State interface {
 | 
			
		||||
 | 
			
		||||
// CopyPaths is a generic implementation of [System.Paths].
 | 
			
		||||
func CopyPaths(os State, v *fst.Paths) {
 | 
			
		||||
	v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
 | 
			
		||||
	v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
 | 
			
		||||
 | 
			
		||||
	fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -31,8 +31,7 @@ type Std struct {
 | 
			
		||||
	uidMu sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Std) Getuid() int                                  { return os.Getuid() }
 | 
			
		||||
func (s *Std) Getgid() int                                  { return os.Getgid() }
 | 
			
		||||
func (s *Std) Geteuid() int                                 { return os.Geteuid() }
 | 
			
		||||
func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
			
		||||
func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
			
		||||
func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								main.go
									
									
									
									
									
								
							@ -20,6 +20,7 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/init0"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
			
		||||
@ -42,6 +43,7 @@ var std sys.State = new(sys.Std)
 | 
			
		||||
func main() {
 | 
			
		||||
	// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
			
		||||
	sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
			
		||||
	init0.TryArgv0()
 | 
			
		||||
 | 
			
		||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
			
		||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
			
		||||
@ -74,7 +76,9 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
			
		||||
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
			
		||||
 | 
			
		||||
	// internal commands
 | 
			
		||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
			
		||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
			
		||||
 | 
			
		||||
	c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
			
		||||
		if len(args) < 1 {
 | 
			
		||||
@ -83,9 +87,10 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
 | 
			
		||||
		// config extraArgs...
 | 
			
		||||
		config := tryPath(args[0])
 | 
			
		||||
		config.Args = append(config.Args, args[1:]...)
 | 
			
		||||
		config.Command = append(config.Command, args[1:]...)
 | 
			
		||||
 | 
			
		||||
		runApp(config)
 | 
			
		||||
		// invoke app
 | 
			
		||||
		runApp(app.MustNew(std), config)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@ -107,8 +112,8 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
		c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
			
		||||
			// initialise config from flags
 | 
			
		||||
			config := &fst.Config{
 | 
			
		||||
				ID:   fid,
 | 
			
		||||
				Args: args,
 | 
			
		||||
				ID:      fid,
 | 
			
		||||
				Command: args,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if aid < 0 || aid > 9999 {
 | 
			
		||||
@ -194,7 +199,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// invoke app
 | 
			
		||||
			runApp(config)
 | 
			
		||||
			runApp(app.MustNew(std), config)
 | 
			
		||||
			panic("unreachable")
 | 
			
		||||
		}).
 | 
			
		||||
			Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
			
		||||
@ -274,11 +279,10 @@ func buildCommand(out io.Writer) command.Command {
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runApp(config *fst.Config) {
 | 
			
		||||
func runApp(a fst.App, config *fst.Config) {
 | 
			
		||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
			
		||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
	defer stop() // unreachable
 | 
			
		||||
	a := app.MustNew(ctx, std)
 | 
			
		||||
 | 
			
		||||
	rs := new(fst.RunState)
 | 
			
		||||
	if sa, err := a.Seal(config); err != nil {
 | 
			
		||||
@ -286,7 +290,7 @@ func runApp(config *fst.Config) {
 | 
			
		||||
		rs.ExitCode = 1
 | 
			
		||||
	} else {
 | 
			
		||||
		// this updates ExitCode
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(rs))
 | 
			
		||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
			
		||||
	}
 | 
			
		||||
	internal.Exit(rs.ExitCode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								nixos.nix
									
									
									
									
									
								
							@ -1,4 +1,3 @@
 | 
			
		||||
packages:
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  pkgs,
 | 
			
		||||
@ -27,7 +26,7 @@ let
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  imports = [ (import ./options.nix packages) ];
 | 
			
		||||
  imports = [ ./options.nix ];
 | 
			
		||||
 | 
			
		||||
  config = mkIf cfg.enable {
 | 
			
		||||
    security.wrappers.fsu = {
 | 
			
		||||
@ -86,11 +85,12 @@ in
 | 
			
		||||
                  enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
 | 
			
		||||
                  conf = {
 | 
			
		||||
                    inherit (app) id;
 | 
			
		||||
                    path = pkgs.writeScript "${app.name}-start" ''
 | 
			
		||||
                      #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
			
		||||
                      ${script}
 | 
			
		||||
                    '';
 | 
			
		||||
                    args = [ "${app.name}-start" ];
 | 
			
		||||
                    command = [
 | 
			
		||||
                      (pkgs.writeScript "${app.name}-start" ''
 | 
			
		||||
                        #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
			
		||||
                        ${script}
 | 
			
		||||
                      '')
 | 
			
		||||
                    ];
 | 
			
		||||
                    confinement = {
 | 
			
		||||
                      app_id = aid;
 | 
			
		||||
                      inherit (app) groups;
 | 
			
		||||
@ -98,15 +98,17 @@ in
 | 
			
		||||
                      home = getsubhome fid aid;
 | 
			
		||||
                      sandbox = {
 | 
			
		||||
                        inherit (app)
 | 
			
		||||
                          devel
 | 
			
		||||
                          userns
 | 
			
		||||
                          net
 | 
			
		||||
                          dev
 | 
			
		||||
                          tty
 | 
			
		||||
                          multiarch
 | 
			
		||||
                          env
 | 
			
		||||
                          ;
 | 
			
		||||
                        syscall = {
 | 
			
		||||
                          inherit (app) compat multiarch bluetooth;
 | 
			
		||||
                          deny_devel = !app.devel;
 | 
			
		||||
                        };
 | 
			
		||||
                        map_real_uid = app.mapRealUid;
 | 
			
		||||
                        no_new_session = app.tty;
 | 
			
		||||
                        direct_wayland = app.insecureWayland;
 | 
			
		||||
                        filesystem =
 | 
			
		||||
                          let
 | 
			
		||||
@ -146,7 +148,7 @@ in
 | 
			
		||||
                          ]
 | 
			
		||||
                          ++ app.extraPaths;
 | 
			
		||||
                        auto_etc = true;
 | 
			
		||||
                        cover = [ "/var/run/nscd" ];
 | 
			
		||||
                        override = [ "/var/run/nscd" ];
 | 
			
		||||
                      };
 | 
			
		||||
                      inherit enablements;
 | 
			
		||||
                      inherit (dbusConfig) session_bus system_bus;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								options.nix
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								options.nix
									
									
									
									
									
								
							@ -1,8 +1,17 @@
 | 
			
		||||
packages:
 | 
			
		||||
{ lib, pkgs, ... }:
 | 
			
		||||
 | 
			
		||||
let
 | 
			
		||||
  inherit (lib) types mkOption mkEnableOption;
 | 
			
		||||
  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
			
		||||
    inherit (pkgs)
 | 
			
		||||
      bubblewrap
 | 
			
		||||
      xdg-dbus-proxy
 | 
			
		||||
      glibc
 | 
			
		||||
      zstd
 | 
			
		||||
      gnutar
 | 
			
		||||
      coreutils
 | 
			
		||||
      ;
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
@ -12,13 +21,13 @@ in
 | 
			
		||||
 | 
			
		||||
      package = mkOption {
 | 
			
		||||
        type = types.package;
 | 
			
		||||
        default = packages.${pkgs.system}.fortify;
 | 
			
		||||
        default = fortify;
 | 
			
		||||
        description = "The fortify package to use.";
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      fsuPackage = mkOption {
 | 
			
		||||
        type = types.package;
 | 
			
		||||
        default = packages.${pkgs.system}.fsu;
 | 
			
		||||
        default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
 | 
			
		||||
        description = "The fsu package to use.";
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -148,19 +157,21 @@ in
 | 
			
		||||
                '';
 | 
			
		||||
              };
 | 
			
		||||
 | 
			
		||||
              devel = mkEnableOption "debugging-related kernel interfaces";
 | 
			
		||||
              userns = mkEnableOption "user namespace creation";
 | 
			
		||||
              nix = mkEnableOption "nix daemon";
 | 
			
		||||
              userns = mkEnableOption "user namespace";
 | 
			
		||||
              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
			
		||||
              dev = mkEnableOption "access to all devices";
 | 
			
		||||
              tty = mkEnableOption "access to the controlling terminal";
 | 
			
		||||
              multiarch = mkEnableOption "multiarch kernel-level support";
 | 
			
		||||
              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
			
		||||
 | 
			
		||||
              net = mkEnableOption "network access" // {
 | 
			
		||||
                default = true;
 | 
			
		||||
              };
 | 
			
		||||
 | 
			
		||||
              nix = mkEnableOption "nix daemon access";
 | 
			
		||||
              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
			
		||||
              dev = mkEnableOption "access to all devices";
 | 
			
		||||
              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
			
		||||
              compat = mkEnableOption "disable syscall filter extensions";
 | 
			
		||||
              devel = mkEnableOption "development kernel APIs";
 | 
			
		||||
              multiarch = mkEnableOption "multiarch kernel support";
 | 
			
		||||
              bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
 | 
			
		||||
 | 
			
		||||
              gpu = mkOption {
 | 
			
		||||
                type = nullOr bool;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								package.nix
									
									
									
									
									
								
							@ -19,13 +19,6 @@
 | 
			
		||||
  gnutar,
 | 
			
		||||
  coreutils,
 | 
			
		||||
 | 
			
		||||
  # for passthru.buildInputs
 | 
			
		||||
  go,
 | 
			
		||||
  gcc,
 | 
			
		||||
 | 
			
		||||
  # for check
 | 
			
		||||
  util-linux,
 | 
			
		||||
 | 
			
		||||
  glibc, # for ldd
 | 
			
		||||
  withStatic ? stdenv.hostPlatform.isStatic,
 | 
			
		||||
}:
 | 
			
		||||
@ -37,7 +30,7 @@ buildGoModule rec {
 | 
			
		||||
  src = builtins.path {
 | 
			
		||||
    name = "${pname}-src";
 | 
			
		||||
    path = lib.cleanSource ./.;
 | 
			
		||||
    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
			
		||||
    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
			
		||||
  };
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
@ -115,14 +108,4 @@ buildGoModule rec {
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
    '';
 | 
			
		||||
 | 
			
		||||
  passthru.targetPkgs =
 | 
			
		||||
    [
 | 
			
		||||
      go
 | 
			
		||||
      gcc
 | 
			
		||||
      xorg.xorgproto
 | 
			
		||||
      util-linux
 | 
			
		||||
    ]
 | 
			
		||||
    ++ buildInputs
 | 
			
		||||
    ++ nativeBuildInputs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								parse.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								parse.go
									
									
									
									
									
								
							@ -50,12 +50,9 @@ func tryPath(name string) (config *fst.Config) {
 | 
			
		||||
 | 
			
		||||
func tryFd(name string) io.ReadCloser {
 | 
			
		||||
	if v, err := strconv.Atoi(name); err != nil {
 | 
			
		||||
		if !errors.Is(err, strconv.ErrSyntax) {
 | 
			
		||||
			fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		fmsg.Verbosef("trying config stream from %d", v)
 | 
			
		||||
		fd := uintptr(v)
 | 
			
		||||
		if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
 | 
			
		||||
			if errors.Is(errno, syscall.EBADF) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								print.go
									
									
									
									
									
								
							@ -89,10 +89,10 @@ func printShowInstance(
 | 
			
		||||
				flags = append(flags, name)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		writeFlag("userns", sandbox.Userns)
 | 
			
		||||
		writeFlag("userns", sandbox.UserNS)
 | 
			
		||||
		writeFlag("net", sandbox.Net)
 | 
			
		||||
		writeFlag("dev", sandbox.Dev)
 | 
			
		||||
		writeFlag("tty", sandbox.Tty)
 | 
			
		||||
		writeFlag("tty", sandbox.NoNewSession)
 | 
			
		||||
		writeFlag("mapuid", sandbox.MapRealUID)
 | 
			
		||||
		writeFlag("directwl", sandbox.DirectWayland)
 | 
			
		||||
		writeFlag("autoetc", sandbox.AutoEtc)
 | 
			
		||||
@ -107,14 +107,14 @@ func printShowInstance(
 | 
			
		||||
		}
 | 
			
		||||
		t.Printf(" Etc:\t%s\n", etc)
 | 
			
		||||
 | 
			
		||||
		if len(sandbox.Cover) > 0 {
 | 
			
		||||
			t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
 | 
			
		||||
		if len(sandbox.Override) > 0 {
 | 
			
		||||
			t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Env           map[string]string   `json:"env"`
 | 
			
		||||
		// Link          [][2]string         `json:"symlink"`
 | 
			
		||||
	}
 | 
			
		||||
	t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
 | 
			
		||||
	t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
 | 
			
		||||
	t.Printf("\n")
 | 
			
		||||
 | 
			
		||||
	if !short {
 | 
			
		||||
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
 | 
			
		||||
		)
 | 
			
		||||
		if e.Config != nil {
 | 
			
		||||
			es = e.Config.Confinement.Enablements.String()
 | 
			
		||||
			cs = fmt.Sprintf("%q", e.Config.Args)
 | 
			
		||||
			cs = fmt.Sprintf("%q", e.Config.Command)
 | 
			
		||||
			as = strconv.Itoa(e.Config.Confinement.AppID)
 | 
			
		||||
		}
 | 
			
		||||
		t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ func Test_printShowInstance(t *testing.T) {
 | 
			
		||||
 Hostname:       "localhost"
 | 
			
		||||
 Flags:          userns net dev tty mapuid autoetc
 | 
			
		||||
 Etc:            /etc
 | 
			
		||||
 Cover:          /var/run/nscd
 | 
			
		||||
 Overrides:      /var/run/nscd
 | 
			
		||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
			
		||||
 | 
			
		||||
Filesystem
 | 
			
		||||
@ -127,7 +127,7 @@ App
 | 
			
		||||
 Hostname:       "localhost"
 | 
			
		||||
 Flags:          userns net dev tty mapuid autoetc
 | 
			
		||||
 Etc:            /etc
 | 
			
		||||
 Cover:          /var/run/nscd
 | 
			
		||||
 Overrides:      /var/run/nscd
 | 
			
		||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
			
		||||
 | 
			
		||||
Filesystem
 | 
			
		||||
@ -192,8 +192,7 @@ App
 | 
			
		||||
  "pid": 3735928559,
 | 
			
		||||
  "config": {
 | 
			
		||||
    "id": "org.chromium.Chromium",
 | 
			
		||||
    "path": "/run/current-system/sw/bin/chromium",
 | 
			
		||||
    "args": [
 | 
			
		||||
    "command": [
 | 
			
		||||
      "chromium",
 | 
			
		||||
      "--ignore-gpu-blocklist",
 | 
			
		||||
      "--disable-smooth-scrolling",
 | 
			
		||||
@ -210,19 +209,24 @@ App
 | 
			
		||||
      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
			
		||||
      "sandbox": {
 | 
			
		||||
        "hostname": "localhost",
 | 
			
		||||
        "seccomp": 32,
 | 
			
		||||
        "devel": true,
 | 
			
		||||
        "userns": true,
 | 
			
		||||
        "net": true,
 | 
			
		||||
        "tty": true,
 | 
			
		||||
        "multiarch": true,
 | 
			
		||||
        "dev": true,
 | 
			
		||||
        "syscall": {
 | 
			
		||||
          "compat": false,
 | 
			
		||||
          "deny_devel": true,
 | 
			
		||||
          "multiarch": true,
 | 
			
		||||
          "linux32": false,
 | 
			
		||||
          "can": false,
 | 
			
		||||
          "bluetooth": false
 | 
			
		||||
        },
 | 
			
		||||
        "no_new_session": true,
 | 
			
		||||
        "map_real_uid": true,
 | 
			
		||||
        "env": {
 | 
			
		||||
          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
			
		||||
          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
			
		||||
          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
			
		||||
        },
 | 
			
		||||
        "map_real_uid": true,
 | 
			
		||||
        "dev": true,
 | 
			
		||||
        "filesystem": [
 | 
			
		||||
          {
 | 
			
		||||
            "src": "/nix/store"
 | 
			
		||||
@ -255,7 +259,7 @@ App
 | 
			
		||||
        ],
 | 
			
		||||
        "etc": "/etc",
 | 
			
		||||
        "auto_etc": true,
 | 
			
		||||
        "cover": [
 | 
			
		||||
        "override": [
 | 
			
		||||
          "/var/run/nscd"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
@ -316,8 +320,7 @@ App
 | 
			
		||||
`},
 | 
			
		||||
		{"json config", nil, fst.Template(), false, true, `{
 | 
			
		||||
  "id": "org.chromium.Chromium",
 | 
			
		||||
  "path": "/run/current-system/sw/bin/chromium",
 | 
			
		||||
  "args": [
 | 
			
		||||
  "command": [
 | 
			
		||||
    "chromium",
 | 
			
		||||
    "--ignore-gpu-blocklist",
 | 
			
		||||
    "--disable-smooth-scrolling",
 | 
			
		||||
@ -334,19 +337,24 @@ App
 | 
			
		||||
    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
			
		||||
    "sandbox": {
 | 
			
		||||
      "hostname": "localhost",
 | 
			
		||||
      "seccomp": 32,
 | 
			
		||||
      "devel": true,
 | 
			
		||||
      "userns": true,
 | 
			
		||||
      "net": true,
 | 
			
		||||
      "tty": true,
 | 
			
		||||
      "multiarch": true,
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "syscall": {
 | 
			
		||||
        "compat": false,
 | 
			
		||||
        "deny_devel": true,
 | 
			
		||||
        "multiarch": true,
 | 
			
		||||
        "linux32": false,
 | 
			
		||||
        "can": false,
 | 
			
		||||
        "bluetooth": false
 | 
			
		||||
      },
 | 
			
		||||
      "no_new_session": true,
 | 
			
		||||
      "map_real_uid": true,
 | 
			
		||||
      "env": {
 | 
			
		||||
        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
			
		||||
        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
			
		||||
        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
			
		||||
      },
 | 
			
		||||
      "map_real_uid": true,
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "filesystem": [
 | 
			
		||||
        {
 | 
			
		||||
          "src": "/nix/store"
 | 
			
		||||
@ -379,7 +387,7 @@ App
 | 
			
		||||
      ],
 | 
			
		||||
      "etc": "/etc",
 | 
			
		||||
      "auto_etc": true,
 | 
			
		||||
      "cover": [
 | 
			
		||||
      "override": [
 | 
			
		||||
        "/var/run/nscd"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
@ -498,8 +506,7 @@ func Test_printPs(t *testing.T) {
 | 
			
		||||
    "pid": 3735928559,
 | 
			
		||||
    "config": {
 | 
			
		||||
      "id": "org.chromium.Chromium",
 | 
			
		||||
      "path": "/run/current-system/sw/bin/chromium",
 | 
			
		||||
      "args": [
 | 
			
		||||
      "command": [
 | 
			
		||||
        "chromium",
 | 
			
		||||
        "--ignore-gpu-blocklist",
 | 
			
		||||
        "--disable-smooth-scrolling",
 | 
			
		||||
@ -516,19 +523,24 @@ func Test_printPs(t *testing.T) {
 | 
			
		||||
        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
			
		||||
        "sandbox": {
 | 
			
		||||
          "hostname": "localhost",
 | 
			
		||||
          "seccomp": 32,
 | 
			
		||||
          "devel": true,
 | 
			
		||||
          "userns": true,
 | 
			
		||||
          "net": true,
 | 
			
		||||
          "tty": true,
 | 
			
		||||
          "multiarch": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "syscall": {
 | 
			
		||||
            "compat": false,
 | 
			
		||||
            "deny_devel": true,
 | 
			
		||||
            "multiarch": true,
 | 
			
		||||
            "linux32": false,
 | 
			
		||||
            "can": false,
 | 
			
		||||
            "bluetooth": false
 | 
			
		||||
          },
 | 
			
		||||
          "no_new_session": true,
 | 
			
		||||
          "map_real_uid": true,
 | 
			
		||||
          "env": {
 | 
			
		||||
            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
			
		||||
            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
			
		||||
            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
			
		||||
          },
 | 
			
		||||
          "map_real_uid": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "filesystem": [
 | 
			
		||||
            {
 | 
			
		||||
              "src": "/nix/store"
 | 
			
		||||
@ -561,7 +573,7 @@ func Test_printPs(t *testing.T) {
 | 
			
		||||
          ],
 | 
			
		||||
          "etc": "/etc",
 | 
			
		||||
          "auto_etc": true,
 | 
			
		||||
          "cover": [
 | 
			
		||||
          "override": [
 | 
			
		||||
            "/var/run/nscd"
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								sandbox/const.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								sandbox/const.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
package sandbox
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
			
		||||
	CAP_SYS_ADMIN       = 0x15
 | 
			
		||||
)
 | 
			
		||||
@ -54,6 +54,7 @@ type (
 | 
			
		||||
		// with behaviour identical to its [exec.Cmd] counterpart.
 | 
			
		||||
		ExtraFiles []*os.File
 | 
			
		||||
 | 
			
		||||
		InitParams
 | 
			
		||||
		// Custom [exec.Cmd] initialisation function.
 | 
			
		||||
		CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
			
		||||
 | 
			
		||||
@ -66,16 +67,14 @@ type (
 | 
			
		||||
		Stdout io.Writer
 | 
			
		||||
		Stderr io.Writer
 | 
			
		||||
 | 
			
		||||
		Cancel    func(cmd *exec.Cmd) error
 | 
			
		||||
		Cancel    func() error
 | 
			
		||||
		WaitDelay time.Duration
 | 
			
		||||
 | 
			
		||||
		cmd *exec.Cmd
 | 
			
		||||
		ctx context.Context
 | 
			
		||||
		Params
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Params holds container configuration and is safe to serialise.
 | 
			
		||||
	Params struct {
 | 
			
		||||
	InitParams struct {
 | 
			
		||||
		// Working directory in the container.
 | 
			
		||||
		Dir string
 | 
			
		||||
		// Initial process environment.
 | 
			
		||||
@ -101,9 +100,7 @@ type (
 | 
			
		||||
 | 
			
		||||
	Ops []Op
 | 
			
		||||
	Op  interface {
 | 
			
		||||
		early(params *Params) error
 | 
			
		||||
		apply(params *Params) error
 | 
			
		||||
		prefix() string
 | 
			
		||||
		apply(params *InitParams) error
 | 
			
		||||
 | 
			
		||||
		Is(op Op) bool
 | 
			
		||||
		fmt.Stringer
 | 
			
		||||
@ -144,12 +141,7 @@ func (p *Container) Start() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
 | 
			
		||||
	p.cmd.WaitDelay = p.WaitDelay
 | 
			
		||||
	if p.Cancel != nil {
 | 
			
		||||
		p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
 | 
			
		||||
	} else {
 | 
			
		||||
		p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
 | 
			
		||||
	}
 | 
			
		||||
	p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay
 | 
			
		||||
	p.cmd.Dir = "/"
 | 
			
		||||
	p.cmd.SysProcAttr = &syscall.SysProcAttr{
 | 
			
		||||
		Setsid:    p.Flags&FAllowTTY == 0,
 | 
			
		||||
@ -191,11 +183,7 @@ func (p *Container) Serve() error {
 | 
			
		||||
		panic("invalid serve")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setup := p.setup
 | 
			
		||||
	p.setup = nil
 | 
			
		||||
 | 
			
		||||
	if p.Path != "" && !path.IsAbs(p.Path) {
 | 
			
		||||
		p.cancel()
 | 
			
		||||
		return msg.WrapErr(syscall.EINVAL,
 | 
			
		||||
			fmt.Sprintf("invalid executable path %q", p.Path))
 | 
			
		||||
	}
 | 
			
		||||
@ -204,7 +192,6 @@ func (p *Container) Serve() error {
 | 
			
		||||
		if p.name == "" {
 | 
			
		||||
			p.Path = os.Getenv("SHELL")
 | 
			
		||||
			if !path.IsAbs(p.Path) {
 | 
			
		||||
				p.cancel()
 | 
			
		||||
				return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
					"no command specified and $SHELL is invalid")
 | 
			
		||||
			}
 | 
			
		||||
@ -212,26 +199,23 @@ func (p *Container) Serve() error {
 | 
			
		||||
		} else if path.IsAbs(p.name) {
 | 
			
		||||
			p.Path = p.name
 | 
			
		||||
		} else if v, err := exec.LookPath(p.name); err != nil {
 | 
			
		||||
			p.cancel()
 | 
			
		||||
			return msg.WrapErr(err, err.Error())
 | 
			
		||||
		} else {
 | 
			
		||||
			p.Path = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := setup.Encode(
 | 
			
		||||
	setup := p.setup
 | 
			
		||||
	p.setup = nil
 | 
			
		||||
	return setup.Encode(
 | 
			
		||||
		&initParams{
 | 
			
		||||
			p.Params,
 | 
			
		||||
			p.InitParams,
 | 
			
		||||
			syscall.Getuid(),
 | 
			
		||||
			syscall.Getgid(),
 | 
			
		||||
			len(p.ExtraFiles),
 | 
			
		||||
			msg.IsVerbose(),
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.cancel()
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
 | 
			
		||||
@ -243,6 +227,6 @@ func (p *Container) String() string {
 | 
			
		||||
 | 
			
		||||
func New(ctx context.Context, name string, args ...string) *Container {
 | 
			
		||||
	return &Container{name: name, ctx: ctx,
 | 
			
		||||
		Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
			
		||||
		InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,10 @@ package sandbox_test
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
@ -18,12 +17,7 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ignore  = "\x00"
 | 
			
		||||
	ignoreV = -1
 | 
			
		||||
	check "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestContainer(t *testing.T) {
 | 
			
		||||
@ -39,7 +33,7 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
		name  string
 | 
			
		||||
		flags sandbox.HardeningFlags
 | 
			
		||||
		ops   *sandbox.Ops
 | 
			
		||||
		mnt   []*vfs.MountInfoEntry
 | 
			
		||||
		mnt   []*check.Mntent
 | 
			
		||||
		host  string
 | 
			
		||||
	}{
 | 
			
		||||
		{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
 | 
			
		||||
@ -48,23 +42,21 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
		{"tmpfs", 0,
 | 
			
		||||
			new(sandbox.Ops).
 | 
			
		||||
				Tmpfs(fst.Tmp, 0, 0755),
 | 
			
		||||
			[]*vfs.MountInfoEntry{
 | 
			
		||||
				e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
			
		||||
			[]*check.Mntent{
 | 
			
		||||
				{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
 | 
			
		||||
			}, "test-tmpfs"},
 | 
			
		||||
		{"dev", sandbox.FAllowTTY, // go test output is not a tty
 | 
			
		||||
			new(sandbox.Ops).
 | 
			
		||||
				Dev("/dev").
 | 
			
		||||
				Mqueue("/dev/mqueue"),
 | 
			
		||||
			[]*vfs.MountInfoEntry{
 | 
			
		||||
				e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
			
		||||
				e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
 | 
			
		||||
				e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
 | 
			
		||||
				Dev("/dev"),
 | 
			
		||||
			[]*check.Mntent{
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
			
		||||
				{FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
 | 
			
		||||
			}, ""},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -73,7 +65,7 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
			defer cancel()
 | 
			
		||||
 | 
			
		||||
			container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
 | 
			
		||||
			container := sandbox.New(ctx, os.Args[0], "-test.v",
 | 
			
		||||
				"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
 | 
			
		||||
			container.Uid = 1000
 | 
			
		||||
			container.Gid = 100
 | 
			
		||||
@ -92,10 +84,7 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
			container.
 | 
			
		||||
				Tmpfs("/tmp", 0, 0755).
 | 
			
		||||
				Bind(os.Args[0], os.Args[0], 0).
 | 
			
		||||
				Mkdir("/usr/bin", 0755).
 | 
			
		||||
				Link(os.Args[0], "/usr/bin/sandbox.test").
 | 
			
		||||
				Place("/etc/hostname", []byte(container.Args[5]))
 | 
			
		||||
				Bind(os.Args[0], os.Args[0], 0)
 | 
			
		||||
			// in case test has cgo enabled
 | 
			
		||||
			var libPaths []string
 | 
			
		||||
			if entries, err := ldd.ExecFilter(ctx,
 | 
			
		||||
@ -110,26 +99,25 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
			for _, name := range libPaths {
 | 
			
		||||
				container.Bind(name, name, 0)
 | 
			
		||||
			}
 | 
			
		||||
			// needs /proc to check mountinfo
 | 
			
		||||
			container.Proc("/proc")
 | 
			
		||||
 | 
			
		||||
			mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
 | 
			
		||||
			mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
 | 
			
		||||
			mnt := make([]*check.Mntent, 0, 3+len(libPaths))
 | 
			
		||||
			mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
 | 
			
		||||
			mnt = append(mnt, tc.mnt...)
 | 
			
		||||
			mnt = append(mnt,
 | 
			
		||||
				e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
			
		||||
				e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
 | 
			
		||||
				e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
 | 
			
		||||
			)
 | 
			
		||||
				&check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
 | 
			
		||||
				&check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
 | 
			
		||||
			for _, name := range libPaths {
 | 
			
		||||
				mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
 | 
			
		||||
				mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1})
 | 
			
		||||
			}
 | 
			
		||||
			mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
 | 
			
		||||
			want := new(bytes.Buffer)
 | 
			
		||||
			if err := gob.NewEncoder(want).Encode(mnt); err != nil {
 | 
			
		||||
				t.Fatalf("cannot serialise expected mount points: %v", err)
 | 
			
		||||
			mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"})
 | 
			
		||||
			mntentWant := new(bytes.Buffer)
 | 
			
		||||
			if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
 | 
			
		||||
				t.Fatalf("cannot serialise mntent: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
			container.Stdin = want
 | 
			
		||||
			container.Stdin = mntentWant
 | 
			
		||||
 | 
			
		||||
			// needs /proc to check mntent
 | 
			
		||||
			container.Proc("/proc")
 | 
			
		||||
 | 
			
		||||
			if err := container.Start(); err != nil {
 | 
			
		||||
				fmsg.PrintBaseError(err, "start:")
 | 
			
		||||
@ -146,21 +134,6 @@ func TestContainer(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
 | 
			
		||||
	return &vfs.MountInfoEntry{
 | 
			
		||||
		ID:        ignoreV,
 | 
			
		||||
		Parent:    ignoreV,
 | 
			
		||||
		Devno:     vfs.DevT{ignoreV, ignoreV},
 | 
			
		||||
		Root:      root,
 | 
			
		||||
		Target:    target,
 | 
			
		||||
		VfsOptstr: vfsOptstr,
 | 
			
		||||
		OptFields: []string{ignore},
 | 
			
		||||
		FsType:    fsType,
 | 
			
		||||
		Source:    source,
 | 
			
		||||
		FsOptstr:  fsOptstr,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestContainerString(t *testing.T) {
 | 
			
		||||
	container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
 | 
			
		||||
	container.Flags |= sandbox.FAllowDevel
 | 
			
		||||
@ -198,55 +171,9 @@ func TestHelperCheckContainer(t *testing.T) {
 | 
			
		||||
		} else if name != os.Args[5] {
 | 
			
		||||
			t.Errorf("Hostname: %q, want %q", name, os.Args[5])
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if p, err := os.ReadFile("/etc/hostname"); err != nil {
 | 
			
		||||
			t.Fatalf("%v", err)
 | 
			
		||||
		} else if string(p) != os.Args[5] {
 | 
			
		||||
			t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("mount", func(t *testing.T) {
 | 
			
		||||
		var mnt []*vfs.MountInfoEntry
 | 
			
		||||
		if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
 | 
			
		||||
			t.Fatalf("cannot receive expected mount points: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var d *vfs.MountInfoDecoder
 | 
			
		||||
		if f, err := os.Open("/proc/self/mountinfo"); err != nil {
 | 
			
		||||
			t.Fatalf("cannot open mountinfo: %v", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			d = vfs.NewMountInfoDecoder(f)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i := 0
 | 
			
		||||
		for cur := range d.Entries() {
 | 
			
		||||
			if i == len(mnt) {
 | 
			
		||||
				t.Errorf("got more than %d entries", len(mnt))
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
 | 
			
		||||
			cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
 | 
			
		||||
			cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
 | 
			
		||||
			mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
 | 
			
		||||
			mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
 | 
			
		||||
 | 
			
		||||
			if !cur.EqualWithIgnore(mnt[i], "\x00") {
 | 
			
		||||
				t.Errorf("[FAIL] %s", cur)
 | 
			
		||||
			} else {
 | 
			
		||||
				t.Logf("[ OK ] %s", cur)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i++
 | 
			
		||||
		}
 | 
			
		||||
		if err := d.Err(); err != nil {
 | 
			
		||||
			t.Errorf("cannot parse mountinfo: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if i != len(mnt) {
 | 
			
		||||
			t.Errorf("got %d entries, want %d", i, len(mnt))
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("seccomp", func(t *testing.T) { check.MustAssertSeccomp() })
 | 
			
		||||
	t.Run("mntent", func(t *testing.T) { check.MustAssertMounts("", "/proc/mounts", "/proc/self/fd/0") })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func commandContext(ctx context.Context) *exec.Cmd {
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type initParams struct {
 | 
			
		||||
	Params
 | 
			
		||||
	InitParams
 | 
			
		||||
 | 
			
		||||
	HostUid, HostGid int
 | 
			
		||||
	// extra files count
 | 
			
		||||
@ -98,7 +98,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldmask := syscall.Umask(0)
 | 
			
		||||
	if params.Hostname != "" {
 | 
			
		||||
		if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
 | 
			
		||||
			log.Fatalf("cannot set hostname: %v", err)
 | 
			
		||||
@ -115,19 +114,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
		log.Fatalf("cannot make / rslave: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, op := range *params.Ops {
 | 
			
		||||
		if op == nil {
 | 
			
		||||
			log.Fatalf("invalid op %d", i)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := op.early(¶ms.Params); err != nil {
 | 
			
		||||
			msg.PrintBaseErr(err,
 | 
			
		||||
				fmt.Sprintf("cannot prepare op %d:", i))
 | 
			
		||||
			msg.BeforeExit()
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := syscall.Mount("rootfs", basePath, "tmpfs",
 | 
			
		||||
		syscall.MS_NODEV|syscall.MS_NOSUID,
 | 
			
		||||
		""); err != nil {
 | 
			
		||||
@ -157,9 +143,8 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, op := range *params.Ops {
 | 
			
		||||
		// ops already checked during early setup
 | 
			
		||||
		msg.Verbosef("%s %s", op.prefix(), op)
 | 
			
		||||
		if err := op.apply(¶ms.Params); err != nil {
 | 
			
		||||
		msg.Verbosef("mounting %s", op)
 | 
			
		||||
		if err := op.apply(¶ms.InitParams); err != nil {
 | 
			
		||||
			msg.PrintBaseErr(err,
 | 
			
		||||
				fmt.Sprintf("cannot apply op %d:", i))
 | 
			
		||||
			msg.BeforeExit()
 | 
			
		||||
@ -231,7 +216,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
	for i := range extraFiles {
 | 
			
		||||
		extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
 | 
			
		||||
	}
 | 
			
		||||
	syscall.Umask(oldmask)
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		prepare initial process
 | 
			
		||||
@ -239,6 +223,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command(params.Path)
 | 
			
		||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
			
		||||
	cmd.Args = params.Args
 | 
			
		||||
	cmd.Env = params.Env
 | 
			
		||||
	cmd.ExtraFiles = extraFiles
 | 
			
		||||
@ -323,13 +308,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
			
		||||
				switch {
 | 
			
		||||
				case w.wstatus.Exited():
 | 
			
		||||
					r = w.wstatus.ExitStatus()
 | 
			
		||||
					msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
 | 
			
		||||
				case w.wstatus.Signaled():
 | 
			
		||||
					r = 128 + int(w.wstatus.Signal())
 | 
			
		||||
					msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
 | 
			
		||||
				default:
 | 
			
		||||
					r = 255
 | 
			
		||||
					msg.Verbosef("initial process exited with status %#x", w.wstatus)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				go func() {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										150
									
								
								sandbox/mount.go
									
									
									
									
									
								
							
							
						
						
									
										150
									
								
								sandbox/mount.go
									
									
									
									
									
								
							@ -4,105 +4,86 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
 | 
			
		||||
	if eq {
 | 
			
		||||
		msg.Verbosef("resolved %q flags %#x", target, flags)
 | 
			
		||||
	} else {
 | 
			
		||||
		msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
 | 
			
		||||
	}
 | 
			
		||||
const (
 | 
			
		||||
	BindOptional = 1 << iota
 | 
			
		||||
	BindSource
 | 
			
		||||
	BindRecursive
 | 
			
		||||
	BindWritable
 | 
			
		||||
	BindDevices
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
	if err := syscall.Mount(source, target, "",
 | 
			
		||||
		syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
 | 
			
		||||
		return wrapErrSuffix(err,
 | 
			
		||||
			fmt.Sprintf("cannot mount %q on %q:", source, target))
 | 
			
		||||
	}
 | 
			
		||||
func bindMount(src, dest string, flags int) error {
 | 
			
		||||
	target := toSysroot(dest)
 | 
			
		||||
	var source string
 | 
			
		||||
 | 
			
		||||
	var targetFinal string
 | 
			
		||||
	if v, err := filepath.EvalSymlinks(target); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
		targetFinal = v
 | 
			
		||||
		if targetFinal != target {
 | 
			
		||||
			msg.Verbosef("target resolves to %q", targetFinal)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// final target path according to the kernel through proc
 | 
			
		||||
	var targetKFinal string
 | 
			
		||||
	{
 | 
			
		||||
		var destFd int
 | 
			
		||||
		if err := IgnoringEINTR(func() (err error) {
 | 
			
		||||
			destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
 | 
			
		||||
			return
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return wrapErrSuffix(err,
 | 
			
		||||
				fmt.Sprintf("cannot open %q:", targetFinal))
 | 
			
		||||
		}
 | 
			
		||||
		if v, err := os.Readlink(p.fd(destFd)); err != nil {
 | 
			
		||||
	if flags&BindSource == 0 {
 | 
			
		||||
		// this is what bwrap does, so the behaviour is kept for now,
 | 
			
		||||
		// however recursively resolving links might improve user experience
 | 
			
		||||
		if rp, err := realpathHost(src); err != nil {
 | 
			
		||||
			if os.IsNotExist(err) {
 | 
			
		||||
				if flags&BindOptional != 0 {
 | 
			
		||||
					return nil
 | 
			
		||||
				} else {
 | 
			
		||||
					return msg.WrapErr(err,
 | 
			
		||||
						fmt.Sprintf("path %q does not exist", src))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return msg.WrapErr(err, err.Error())
 | 
			
		||||
		} else if err = syscall.Close(destFd); err != nil {
 | 
			
		||||
			return wrapErrSuffix(err,
 | 
			
		||||
				fmt.Sprintf("cannot close %q:", targetFinal))
 | 
			
		||||
		} else {
 | 
			
		||||
			targetKFinal = v
 | 
			
		||||
			source = toHost(rp)
 | 
			
		||||
		}
 | 
			
		||||
	} else if flags&BindOptional != 0 {
 | 
			
		||||
		return msg.WrapErr(syscall.EINVAL,
 | 
			
		||||
			"flag source excludes optional")
 | 
			
		||||
	} else {
 | 
			
		||||
		source = toHost(src)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
 | 
			
		||||
	return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
 | 
			
		||||
		n, err := d.Unfold(targetKFinal)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if errors.Is(err, syscall.ESTALE) {
 | 
			
		||||
				return msg.WrapErr(err,
 | 
			
		||||
					fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
 | 
			
		||||
			}
 | 
			
		||||
	if fi, err := os.Stat(source); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else if fi.IsDir() {
 | 
			
		||||
		if err = os.MkdirAll(target, 0755); err != nil {
 | 
			
		||||
			return wrapErrSuffix(err,
 | 
			
		||||
				"cannot unfold mount hierarchy:")
 | 
			
		||||
				fmt.Sprintf("cannot create directory %q:", dest))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = remountWithFlags(n, mf); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
	} else if err = ensureFile(target, 0444); err != nil {
 | 
			
		||||
		if errors.Is(err, syscall.EISDIR) {
 | 
			
		||||
			return msg.WrapErr(err,
 | 
			
		||||
				fmt.Sprintf("path %q is a directory", dest))
 | 
			
		||||
		}
 | 
			
		||||
		if flags&syscall.MS_REC == 0 {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for cur := range n.Collective() {
 | 
			
		||||
			err = remountWithFlags(cur, mf)
 | 
			
		||||
			if err != nil && !errors.Is(err, syscall.EACCES) {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
 | 
			
		||||
	kf, unmatched := n.Flags()
 | 
			
		||||
	if len(unmatched) != 0 {
 | 
			
		||||
		msg.Verbosef("unmatched vfs options: %q", unmatched)
 | 
			
		||||
		return wrapErrSuffix(err,
 | 
			
		||||
			fmt.Sprintf("cannot create %q:", dest))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if kf&mf != mf {
 | 
			
		||||
		return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
 | 
			
		||||
			syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
 | 
			
		||||
			""),
 | 
			
		||||
			fmt.Sprintf("cannot remount %q:", n.Clean))
 | 
			
		||||
	var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
 | 
			
		||||
	if flags&BindRecursive != 0 {
 | 
			
		||||
		mf |= syscall.MS_REC
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	if flags&BindWritable == 0 {
 | 
			
		||||
		mf |= syscall.MS_RDONLY
 | 
			
		||||
	}
 | 
			
		||||
	if flags&BindDevices == 0 {
 | 
			
		||||
		mf |= syscall.MS_NODEV
 | 
			
		||||
	}
 | 
			
		||||
	if msg.IsVerbose() {
 | 
			
		||||
		if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
 | 
			
		||||
			msg.Verbosef("resolved %q flags %#x", target, mf)
 | 
			
		||||
		} else {
 | 
			
		||||
			msg.Verbosef("resolved %q on %q flags %#x", source, target, mf)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
 | 
			
		||||
		fmt.Sprintf("cannot bind %q on %q:", src, dest))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
			
		||||
	target := toSysroot(name)
 | 
			
		||||
	if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	if err := os.MkdirAll(target, perm); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	opt := fmt.Sprintf("mode=%#o", perm)
 | 
			
		||||
	if size > 0 {
 | 
			
		||||
@ -112,14 +93,3 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
			
		||||
		syscall.MS_NOSUID|syscall.MS_NODEV, opt),
 | 
			
		||||
		fmt.Sprintf("cannot mount tmpfs on %q:", name))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parentPerm(perm os.FileMode) os.FileMode {
 | 
			
		||||
	pperm := 0755
 | 
			
		||||
	if perm&0070 == 0 {
 | 
			
		||||
		pperm &= ^0050
 | 
			
		||||
	}
 | 
			
		||||
	if perm&0007 == 0 {
 | 
			
		||||
		pperm &= ^0005
 | 
			
		||||
	}
 | 
			
		||||
	return os.FileMode(pperm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,15 +2,11 @@ package sandbox
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@ -30,65 +26,50 @@ func toHost(name string) string {
 | 
			
		||||
	return path.Join(hostPath, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
 | 
			
		||||
	if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
func realpathHost(name string) (string, error) {
 | 
			
		||||
	source := toHost(name)
 | 
			
		||||
	rp, err := os.Readlink(source)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, syscall.EINVAL) {
 | 
			
		||||
			// not a symlink
 | 
			
		||||
			return name, nil
 | 
			
		||||
		}
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !path.IsAbs(rp) {
 | 
			
		||||
		return name, nil
 | 
			
		||||
	}
 | 
			
		||||
	msg.Verbosef("path %q resolves to %q", name, rp)
 | 
			
		||||
	return rp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createFile(name string, perm os.FileMode, content []byte) error {
 | 
			
		||||
	if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if content != nil {
 | 
			
		||||
		_, err = f.Write(content)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			err = msg.WrapErr(err, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return errors.Join(f.Close(), err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureFile(name string, perm, pperm os.FileMode) error {
 | 
			
		||||
func ensureFile(name string, perm os.FileMode) error {
 | 
			
		||||
	fi, err := os.Stat(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !os.IsNotExist(err) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return createFile(name, perm, pperm, nil)
 | 
			
		||||
		return createFile(name, perm, nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
 | 
			
		||||
		err = msg.WrapErr(syscall.EISDIR,
 | 
			
		||||
			fmt.Sprintf("path %q is a directory", name))
 | 
			
		||||
		err = syscall.EISDIR
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var hostProc = newProcPats(hostPath)
 | 
			
		||||
 | 
			
		||||
func newProcPats(prefix string) *procPaths {
 | 
			
		||||
	return &procPaths{prefix + "/proc", prefix + "/proc/self"}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type procPaths struct {
 | 
			
		||||
	prefix string
 | 
			
		||||
	self   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *procPaths) stdout() string   { return p.self + "/fd/1" }
 | 
			
		||||
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
 | 
			
		||||
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
 | 
			
		||||
	if r, err := os.Open(p.self + "/mountinfo"); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
		d := vfs.NewMountInfoDecoder(r)
 | 
			
		||||
		err0 := f(d)
 | 
			
		||||
		if err = r.Close(); err != nil {
 | 
			
		||||
			return wrapErrSuffix(err,
 | 
			
		||||
				"cannot close mountinfo:")
 | 
			
		||||
		} else if err = d.Err(); err != nil {
 | 
			
		||||
			return wrapErrSuffix(err,
 | 
			
		||||
				"cannot parse mountinfo:")
 | 
			
		||||
		}
 | 
			
		||||
		return err0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -93,7 +93,7 @@ func TestExport(t *testing.T) {
 | 
			
		||||
				t.Errorf("Close: error = %v", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
 | 
			
		||||
			if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 {
 | 
			
		||||
				t.Fatalf("Export() hash = %x, want %x",
 | 
			
		||||
					got, tc.want)
 | 
			
		||||
				return
 | 
			
		||||
@ -111,14 +111,11 @@ func TestExport(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	t.Run("close partial read", func(t *testing.T) {
 | 
			
		||||
		e := seccomp.New(0)
 | 
			
		||||
		if _, err := e.Read(nil); err != nil {
 | 
			
		||||
		if _, err := e.Read(make([]byte, 0)); err != nil {
 | 
			
		||||
			t.Errorf("Read: error = %v", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
 | 
			
		||||
		// that is not harmful however, so both outcomes are checked for here
 | 
			
		||||
		if err := e.Close(); err != nil &&
 | 
			
		||||
			(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
 | 
			
		||||
		if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) {
 | 
			
		||||
			t.Errorf("Close: error = %v", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,6 @@ import (
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
)
 | 
			
		||||
@ -16,77 +14,20 @@ func init() { gob.Register(new(BindMount)) }
 | 
			
		||||
 | 
			
		||||
// BindMount bind mounts host path Source on container path Target.
 | 
			
		||||
type BindMount struct {
 | 
			
		||||
	Source, SourceFinal, Target string
 | 
			
		||||
	Source, Target string
 | 
			
		||||
 | 
			
		||||
	Flags int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	BindOptional = 1 << iota
 | 
			
		||||
	BindWritable
 | 
			
		||||
	BindDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *BindMount) early(*Params) error {
 | 
			
		||||
	if !path.IsAbs(b.Source) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", b.Source))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, err := filepath.EvalSymlinks(b.Source); err != nil {
 | 
			
		||||
		if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
 | 
			
		||||
			b.SourceFinal = "\x00"
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
		b.SourceFinal = v
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *BindMount) apply(*Params) error {
 | 
			
		||||
	if b.SourceFinal == "\x00" {
 | 
			
		||||
		if b.Flags&BindOptional == 0 {
 | 
			
		||||
			// unreachable
 | 
			
		||||
			return syscall.EBADE
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
 | 
			
		||||
func (b *BindMount) apply(*InitParams) error {
 | 
			
		||||
	if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			"path is not absolute")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	source := toHost(b.SourceFinal)
 | 
			
		||||
	target := toSysroot(b.Target)
 | 
			
		||||
 | 
			
		||||
	// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
 | 
			
		||||
	// op->perms which is never set for any bind setup op so always results in 0700
 | 
			
		||||
	if fi, err := os.Stat(source); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else if fi.IsDir() {
 | 
			
		||||
		if err = os.MkdirAll(target, 0700); err != nil {
 | 
			
		||||
			return msg.WrapErr(err, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	} else if err = ensureFile(target, 0444, 0700); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var flags uintptr = syscall.MS_REC
 | 
			
		||||
	if b.Flags&BindWritable == 0 {
 | 
			
		||||
		flags |= syscall.MS_RDONLY
 | 
			
		||||
	}
 | 
			
		||||
	if b.Flags&BindDevice == 0 {
 | 
			
		||||
		flags |= syscall.MS_NODEV
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
 | 
			
		||||
	return bindMount(b.Source, b.Target, b.Flags)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
 | 
			
		||||
func (*BindMount) prefix() string  { return "mounting" }
 | 
			
		||||
func (b *BindMount) String() string {
 | 
			
		||||
	if b.Source == b.Target {
 | 
			
		||||
		return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
 | 
			
		||||
@ -94,70 +35,54 @@ func (b *BindMount) String() string {
 | 
			
		||||
	return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
 | 
			
		||||
}
 | 
			
		||||
func (f *Ops) Bind(source, target string, flags int) *Ops {
 | 
			
		||||
	*f = append(*f, &BindMount{source, "", target, flags})
 | 
			
		||||
	*f = append(*f, &BindMount{source, target, flags | BindRecursive})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(MountProc)) }
 | 
			
		||||
 | 
			
		||||
// MountProc mounts a private instance of proc.
 | 
			
		||||
type MountProc string
 | 
			
		||||
// MountProc mounts a private proc instance on container Path.
 | 
			
		||||
type MountProc struct {
 | 
			
		||||
	Path string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p MountProc) early(*Params) error { return nil }
 | 
			
		||||
func (p MountProc) apply(*Params) error {
 | 
			
		||||
	v := string(p)
 | 
			
		||||
 | 
			
		||||
	if !path.IsAbs(v) {
 | 
			
		||||
func (p *MountProc) apply(*InitParams) error {
 | 
			
		||||
	if !path.IsAbs(p.Path) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", v))
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", p.Path))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target := toSysroot(v)
 | 
			
		||||
	target := toSysroot(p.Path)
 | 
			
		||||
	if err := os.MkdirAll(target, 0755); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return wrapErrSuffix(syscall.Mount("proc", target, "proc",
 | 
			
		||||
		syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
 | 
			
		||||
		fmt.Sprintf("cannot mount proc on %q:", v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p MountProc) Is(op Op) bool  { vp, ok := op.(MountProc); return ok && p == vp }
 | 
			
		||||
func (MountProc) prefix() string   { return "mounting" }
 | 
			
		||||
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
 | 
			
		||||
func (f *Ops) Proc(dest string) *Ops {
 | 
			
		||||
	*f = append(*f, MountProc(dest))
 | 
			
		||||
	return f
 | 
			
		||||
		fmt.Sprintf("cannot mount proc on %q:", p.Path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(MountDev)) }
 | 
			
		||||
 | 
			
		||||
// MountDev mounts part of host dev.
 | 
			
		||||
type MountDev string
 | 
			
		||||
// MountDev mounts dev on container Path.
 | 
			
		||||
type MountDev struct {
 | 
			
		||||
	Path string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d MountDev) early(*Params) error { return nil }
 | 
			
		||||
func (d MountDev) apply(params *Params) error {
 | 
			
		||||
	v := string(d)
 | 
			
		||||
 | 
			
		||||
	if !path.IsAbs(v) {
 | 
			
		||||
func (d *MountDev) apply(params *InitParams) error {
 | 
			
		||||
	if !path.IsAbs(d.Path) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", v))
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", d.Path))
 | 
			
		||||
	}
 | 
			
		||||
	target := toSysroot(v)
 | 
			
		||||
	target := toSysroot(d.Path)
 | 
			
		||||
 | 
			
		||||
	if err := mountTmpfs("devtmpfs", v, 0, 0755); err != nil {
 | 
			
		||||
	if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
 | 
			
		||||
		targetPath := toSysroot(path.Join(v, name))
 | 
			
		||||
		if err := ensureFile(targetPath, 0444, 0755); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := hostProc.bindMount(
 | 
			
		||||
			toHost("/dev/"+name),
 | 
			
		||||
			targetPath,
 | 
			
		||||
			0,
 | 
			
		||||
			true,
 | 
			
		||||
		if err := bindMount(
 | 
			
		||||
			"/dev/"+name, path.Join(d.Path, name),
 | 
			
		||||
			BindSource|BindDevices,
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@ -200,17 +125,9 @@ func (d MountDev) apply(params *Params) error {
 | 
			
		||||
			syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
 | 
			
		||||
			uintptr(unsafe.Pointer(&buf[0])),
 | 
			
		||||
		); errno == 0 {
 | 
			
		||||
			consolePath := toSysroot(path.Join(v, "console"))
 | 
			
		||||
			if err := ensureFile(consolePath, 0444, 0755); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if name, err := os.Readlink(hostProc.stdout()); err != nil {
 | 
			
		||||
				return msg.WrapErr(err, err.Error())
 | 
			
		||||
			} else if err = hostProc.bindMount(
 | 
			
		||||
				toHost(name),
 | 
			
		||||
				consolePath,
 | 
			
		||||
				0,
 | 
			
		||||
				false,
 | 
			
		||||
			if err := bindMount(
 | 
			
		||||
				"/proc/self/fd/1", path.Join(d.Path, "console"),
 | 
			
		||||
				BindDevices,
 | 
			
		||||
			); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
@ -220,42 +137,17 @@ func (d MountDev) apply(params *Params) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d MountDev) Is(op Op) bool  { vd, ok := op.(MountDev); return ok && d == vd }
 | 
			
		||||
func (MountDev) prefix() string   { return "mounting" }
 | 
			
		||||
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
 | 
			
		||||
func (d *MountDev) Is(op Op) bool  { vd, ok := op.(*MountDev); return ok && *d == *vd }
 | 
			
		||||
func (d *MountDev) String() string { return fmt.Sprintf("dev on %q", d.Path) }
 | 
			
		||||
func (f *Ops) Dev(dest string) *Ops {
 | 
			
		||||
	*f = append(*f, MountDev(dest))
 | 
			
		||||
	*f = append(*f, &MountDev{dest})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(MountMqueue)) }
 | 
			
		||||
 | 
			
		||||
// MountMqueue mounts a private mqueue instance on container Path.
 | 
			
		||||
type MountMqueue string
 | 
			
		||||
 | 
			
		||||
func (m MountMqueue) early(*Params) error { return nil }
 | 
			
		||||
func (m MountMqueue) apply(*Params) error {
 | 
			
		||||
	v := string(m)
 | 
			
		||||
 | 
			
		||||
	if !path.IsAbs(v) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", v))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target := toSysroot(v)
 | 
			
		||||
	if err := os.MkdirAll(target, 0755); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return wrapErrSuffix(syscall.Mount("mqueue", target, "mqueue",
 | 
			
		||||
		syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
 | 
			
		||||
		fmt.Sprintf("cannot mount mqueue on %q:", v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m MountMqueue) Is(op Op) bool  { vm, ok := op.(MountMqueue); return ok && m == vm }
 | 
			
		||||
func (MountMqueue) prefix() string   { return "mounting" }
 | 
			
		||||
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
 | 
			
		||||
func (f *Ops) Mqueue(dest string) *Ops {
 | 
			
		||||
	*f = append(*f, MountMqueue(dest))
 | 
			
		||||
func (p *MountProc) Is(op Op) bool  { vp, ok := op.(*MountProc); return ok && *p == *vp }
 | 
			
		||||
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
 | 
			
		||||
func (f *Ops) Proc(dest string) *Ops {
 | 
			
		||||
	*f = append(*f, &MountProc{dest})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -268,8 +160,7 @@ type MountTmpfs struct {
 | 
			
		||||
	Perm os.FileMode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *MountTmpfs) early(*Params) error { return nil }
 | 
			
		||||
func (t *MountTmpfs) apply(*Params) error {
 | 
			
		||||
func (t *MountTmpfs) apply(*InitParams) error {
 | 
			
		||||
	if !path.IsAbs(t.Path) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", t.Path))
 | 
			
		||||
@ -282,133 +173,8 @@ func (t *MountTmpfs) apply(*Params) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *MountTmpfs) Is(op Op) bool  { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
 | 
			
		||||
func (*MountTmpfs) prefix() string   { return "mounting" }
 | 
			
		||||
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
 | 
			
		||||
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
 | 
			
		||||
	*f = append(*f, &MountTmpfs{dest, size, perm})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(Symlink)) }
 | 
			
		||||
 | 
			
		||||
// Symlink creates a symlink in the container filesystem.
 | 
			
		||||
type Symlink [2]string
 | 
			
		||||
 | 
			
		||||
func (l *Symlink) early(*Params) error { return nil }
 | 
			
		||||
func (l *Symlink) apply(*Params) error {
 | 
			
		||||
	// symlink target is an arbitrary path value, so only validate link name here
 | 
			
		||||
	if !path.IsAbs(l[1]) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", l[1]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target := toSysroot(l[1])
 | 
			
		||||
	if err := ensureFile(target, 0444, 0755); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := os.Remove(target); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	if err := os.Symlink(l[0], target); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *Symlink) Is(op Op) bool  { vl, ok := op.(*Symlink); return ok && *l == *vl }
 | 
			
		||||
func (*Symlink) prefix() string   { return "creating" }
 | 
			
		||||
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
 | 
			
		||||
func (f *Ops) Link(target, linkName string) *Ops {
 | 
			
		||||
	*f = append(*f, &Symlink{target, linkName})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(Mkdir)) }
 | 
			
		||||
 | 
			
		||||
// Mkdir creates a directory in the container filesystem.
 | 
			
		||||
type Mkdir struct {
 | 
			
		||||
	Path string
 | 
			
		||||
	Perm os.FileMode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mkdir) early(*Params) error { return nil }
 | 
			
		||||
func (m *Mkdir) apply(*Params) error {
 | 
			
		||||
	if !path.IsAbs(m.Path) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", m.Path))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mkdir) Is(op Op) bool  { vm, ok := op.(*Mkdir); return ok && m == vm }
 | 
			
		||||
func (*Mkdir) prefix() string   { return "creating" }
 | 
			
		||||
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
 | 
			
		||||
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
 | 
			
		||||
	*f = append(*f, &Mkdir{dest, perm})
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { gob.Register(new(Tmpfile)) }
 | 
			
		||||
 | 
			
		||||
// Tmpfile places a file in container Path containing Data.
 | 
			
		||||
type Tmpfile struct {
 | 
			
		||||
	Path string
 | 
			
		||||
	Data []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Tmpfile) early(*Params) error { return nil }
 | 
			
		||||
func (t *Tmpfile) apply(*Params) error {
 | 
			
		||||
	if !path.IsAbs(t.Path) {
 | 
			
		||||
		return msg.WrapErr(syscall.EBADE,
 | 
			
		||||
			fmt.Sprintf("path %q is not absolute", t.Path))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var tmpPath string
 | 
			
		||||
	if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	} else if _, err = f.Write(t.Data); err != nil {
 | 
			
		||||
		return wrapErrSuffix(err,
 | 
			
		||||
			"cannot write to intermediate file:")
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		return wrapErrSuffix(err,
 | 
			
		||||
			"cannot close intermediate file:")
 | 
			
		||||
	} else {
 | 
			
		||||
		tmpPath = f.Name()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target := toSysroot(t.Path)
 | 
			
		||||
	if err := ensureFile(target, 0444, 0755); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else if err = hostProc.bindMount(
 | 
			
		||||
		tmpPath,
 | 
			
		||||
		target,
 | 
			
		||||
		syscall.MS_RDONLY|syscall.MS_NODEV,
 | 
			
		||||
		false,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else if err = os.Remove(tmpPath); err != nil {
 | 
			
		||||
		return msg.WrapErr(err, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Tmpfile) Is(op Op) bool {
 | 
			
		||||
	vt, ok := op.(*Tmpfile)
 | 
			
		||||
	return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
 | 
			
		||||
}
 | 
			
		||||
func (*Tmpfile) prefix() string { return "placing" }
 | 
			
		||||
func (t *Tmpfile) String() string {
 | 
			
		||||
	return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
 | 
			
		||||
}
 | 
			
		||||
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &Tmpfile{name, data}); return f }
 | 
			
		||||
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
 | 
			
		||||
	t := &Tmpfile{Path: name}
 | 
			
		||||
	*dataP = &t.Data
 | 
			
		||||
 | 
			
		||||
	*f = append(*f, t)
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,12 +2,6 @@ package sandbox
 | 
			
		||||
 | 
			
		||||
import "syscall"
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	O_PATH              = 0x200000
 | 
			
		||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
			
		||||
	CAP_SYS_ADMIN       = 0x15
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SUID_DUMP_DISABLE = iota
 | 
			
		||||
	SUID_DUMP_USER
 | 
			
		||||
@ -22,6 +16,14 @@ func SetDumpable(dumpable uintptr) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetPdeathsig(sig syscall.Signal) error {
 | 
			
		||||
	if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 {
 | 
			
		||||
		return errno
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IgnoringEINTR makes a function call and repeats it if it returns an
 | 
			
		||||
// EINTR error. This appears to be required even though we install all
 | 
			
		||||
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
 | 
			
		||||
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
package vfs
 | 
			
		||||
 | 
			
		||||
import "strings"
 | 
			
		||||
 | 
			
		||||
func Unmangle(s string) string {
 | 
			
		||||
	if !strings.ContainsRune(s, '\\') {
 | 
			
		||||
		return s
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v := make([]byte, len(s))
 | 
			
		||||
	var (
 | 
			
		||||
		j int
 | 
			
		||||
		c byte
 | 
			
		||||
	)
 | 
			
		||||
	for i := 0; i < len(s); i++ {
 | 
			
		||||
		c = s[i]
 | 
			
		||||
		if c == '\\' && len(s) > i+3 &&
 | 
			
		||||
			(s[i+1] == '0' || s[i+1] == '1') &&
 | 
			
		||||
			(s[i+2] >= '0' && s[i+2] <= '7') &&
 | 
			
		||||
			(s[i+3] >= '0' && s[i+3] <= '7') {
 | 
			
		||||
			c = ((s[i+1] - '0') << 6) |
 | 
			
		||||
				((s[i+2] - '0') << 3) |
 | 
			
		||||
				(s[i+3] - '0')
 | 
			
		||||
			i += 3
 | 
			
		||||
		}
 | 
			
		||||
		v[j] = c
 | 
			
		||||
		j++
 | 
			
		||||
	}
 | 
			
		||||
	return string(v[:j])
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
package vfs_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestUnmangle(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		want   string
 | 
			
		||||
		sample string
 | 
			
		||||
	}{
 | 
			
		||||
		{`\, `, `\134\054\040`},
 | 
			
		||||
		{`(10) source -- maybe empty string`, `(10)\040source\040--\040maybe empty string`},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.want, func(t *testing.T) {
 | 
			
		||||
			got := vfs.Unmangle(tc.sample)
 | 
			
		||||
			if got != tc.want {
 | 
			
		||||
				t.Errorf("Unmangle: %q, want %q",
 | 
			
		||||
					got, tc.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,260 +0,0 @@
 | 
			
		||||
// Package vfs provides bindings and iterators over proc_pid_mountinfo(5).
 | 
			
		||||
package vfs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	MS_NOSYMFOLLOW = 0x100
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrMountInfoFields = errors.New("unexpected field count")
 | 
			
		||||
	ErrMountInfoEmpty  = errors.New("unexpected empty field")
 | 
			
		||||
	ErrMountInfoDevno  = errors.New("bad maj:min field")
 | 
			
		||||
	ErrMountInfoSep    = errors.New("bad optional fields separator")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
 | 
			
		||||
	MountInfoDecoder struct {
 | 
			
		||||
		s *bufio.Scanner
 | 
			
		||||
		m *MountInfo
 | 
			
		||||
 | 
			
		||||
		current  *MountInfo
 | 
			
		||||
		parseErr error
 | 
			
		||||
		complete bool
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// MountInfo represents the contents of a proc_pid_mountinfo(5) document.
 | 
			
		||||
	MountInfo struct {
 | 
			
		||||
		Next *MountInfo
 | 
			
		||||
		MountInfoEntry
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// MountInfoEntry represents a proc_pid_mountinfo(5) entry.
 | 
			
		||||
	MountInfoEntry struct {
 | 
			
		||||
		// mount ID: a unique ID for the mount (may be reused after umount(2)).
 | 
			
		||||
		ID int `json:"id"`
 | 
			
		||||
		// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
 | 
			
		||||
		Parent int `json:"parent"`
 | 
			
		||||
		// major:minor: the value of st_dev for files on this filesystem (see stat(2)).
 | 
			
		||||
		Devno DevT `json:"devno"`
 | 
			
		||||
		// root: the pathname of the directory in the filesystem which forms the root of this mount.
 | 
			
		||||
		Root string `json:"root"`
 | 
			
		||||
		// mount point: the pathname of the mount point relative to the process's root directory.
 | 
			
		||||
		Target string `json:"target"`
 | 
			
		||||
		// mount options: per-mount options (see mount(2)).
 | 
			
		||||
		VfsOptstr string `json:"vfs_optstr"`
 | 
			
		||||
		// optional fields: zero or more fields of the form "tag[:value]"; see below.
 | 
			
		||||
		// separator: the end of the optional fields is marked by a single hyphen.
 | 
			
		||||
		OptFields []string `json:"opt_fields"`
 | 
			
		||||
		// filesystem type: the filesystem type in the form "type[.subtype]".
 | 
			
		||||
		FsType string `json:"fstype"`
 | 
			
		||||
		// mount source: filesystem-specific information or "none".
 | 
			
		||||
		Source string `json:"source"`
 | 
			
		||||
		// super options: per-superblock options (see mount(2)).
 | 
			
		||||
		FsOptstr string `json:"fs_optstr"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	DevT [2]int
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Flags interprets VfsOptstr and returns the resulting flags and unmatched options.
 | 
			
		||||
func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) {
 | 
			
		||||
	for _, s := range strings.Split(e.VfsOptstr, ",") {
 | 
			
		||||
		switch s {
 | 
			
		||||
		case "rw":
 | 
			
		||||
		case "ro":
 | 
			
		||||
			flags |= syscall.MS_RDONLY
 | 
			
		||||
		case "nosuid":
 | 
			
		||||
			flags |= syscall.MS_NOSUID
 | 
			
		||||
		case "nodev":
 | 
			
		||||
			flags |= syscall.MS_NODEV
 | 
			
		||||
		case "noexec":
 | 
			
		||||
			flags |= syscall.MS_NOEXEC
 | 
			
		||||
		case "nosymfollow":
 | 
			
		||||
			flags |= MS_NOSYMFOLLOW
 | 
			
		||||
		case "noatime":
 | 
			
		||||
			flags |= syscall.MS_NOATIME
 | 
			
		||||
		case "nodiratime":
 | 
			
		||||
			flags |= syscall.MS_NODIRATIME
 | 
			
		||||
		case "relatime":
 | 
			
		||||
			flags |= syscall.MS_RELATIME
 | 
			
		||||
		default:
 | 
			
		||||
			unmatched = append(unmatched, s)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewMountInfoDecoder returns a new decoder that reads from r.
 | 
			
		||||
//
 | 
			
		||||
// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested.
 | 
			
		||||
func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder {
 | 
			
		||||
	return &MountInfoDecoder{s: bufio.NewScanner(r)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) {
 | 
			
		||||
	for d.scan() {
 | 
			
		||||
	}
 | 
			
		||||
	err = d.Err()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		*v = d.m
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Entries returns an iterator over mountinfo entries.
 | 
			
		||||
func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
 | 
			
		||||
	return func(yield func(*MountInfoEntry) bool) {
 | 
			
		||||
		for cur := d.m; cur != nil; cur = cur.Next {
 | 
			
		||||
			if !yield(&cur.MountInfoEntry) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for d.scan() {
 | 
			
		||||
			if !yield(&d.current.MountInfoEntry) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *MountInfoDecoder) Err() error {
 | 
			
		||||
	if err := d.s.Err(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return d.parseErr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *MountInfoDecoder) scan() bool {
 | 
			
		||||
	if d.complete {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if !d.s.Scan() {
 | 
			
		||||
		d.complete = true
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m := new(MountInfo)
 | 
			
		||||
	if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil {
 | 
			
		||||
		d.parseErr = err
 | 
			
		||||
		d.complete = true
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if d.current == nil {
 | 
			
		||||
		d.m = m
 | 
			
		||||
		d.current = d.m
 | 
			
		||||
	} else {
 | 
			
		||||
		d.current.Next = m
 | 
			
		||||
		d.current = d.current.Next
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseMountInfoLine(s string, ent *MountInfoEntry) error {
 | 
			
		||||
	// prevent proceeding with misaligned fields due to optional fields
 | 
			
		||||
	f := strings.Split(s, " ")
 | 
			
		||||
	if len(f) < 10 {
 | 
			
		||||
		return ErrMountInfoFields
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 | 
			
		||||
	// (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
 | 
			
		||||
 | 
			
		||||
	// (1) id
 | 
			
		||||
	if id, err := strconv.Atoi(f[0]); err != nil { // 0
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		ent.ID = id
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (2) parent
 | 
			
		||||
	if parent, err := strconv.Atoi(f[1]); err != nil { // 1
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		ent.Parent = parent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (3) maj:min
 | 
			
		||||
	if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else if n != 2 {
 | 
			
		||||
		// unreachable
 | 
			
		||||
		return ErrMountInfoDevno
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (4) mountroot
 | 
			
		||||
	ent.Root = Unmangle(f[3])
 | 
			
		||||
	if ent.Root == "" {
 | 
			
		||||
		return ErrMountInfoEmpty
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (5) target
 | 
			
		||||
	ent.Target = Unmangle(f[4])
 | 
			
		||||
	if ent.Target == "" {
 | 
			
		||||
		return ErrMountInfoEmpty
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (6) vfs options (fs-independent)
 | 
			
		||||
	ent.VfsOptstr = Unmangle(f[5])
 | 
			
		||||
	if ent.VfsOptstr == "" {
 | 
			
		||||
		return ErrMountInfoEmpty
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (7) optional fields, terminated by " - "
 | 
			
		||||
	i := len(f) - 4
 | 
			
		||||
	ent.OptFields = f[6:i]
 | 
			
		||||
 | 
			
		||||
	// (8) optional fields end marker
 | 
			
		||||
	if f[i] != "-" {
 | 
			
		||||
		return ErrMountInfoSep
 | 
			
		||||
	}
 | 
			
		||||
	i++
 | 
			
		||||
 | 
			
		||||
	// (9) FS type
 | 
			
		||||
	ent.FsType = Unmangle(f[i])
 | 
			
		||||
	if ent.FsType == "" {
 | 
			
		||||
		return ErrMountInfoEmpty
 | 
			
		||||
	}
 | 
			
		||||
	i++
 | 
			
		||||
 | 
			
		||||
	// (10) source -- maybe empty string
 | 
			
		||||
	ent.Source = Unmangle(f[i])
 | 
			
		||||
	i++
 | 
			
		||||
 | 
			
		||||
	// (11) fs options (fs specific)
 | 
			
		||||
	ent.FsOptstr = Unmangle(f[i])
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool {
 | 
			
		||||
	return (e.ID == want.ID || want.ID == -1) &&
 | 
			
		||||
		(e.Parent == want.Parent || want.Parent == -1) &&
 | 
			
		||||
		(e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) &&
 | 
			
		||||
		(e.Root == want.Root || want.Root == ignore) &&
 | 
			
		||||
		(e.Target == want.Target || want.Target == ignore) &&
 | 
			
		||||
		(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
 | 
			
		||||
		(slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) &&
 | 
			
		||||
		(e.FsType == want.FsType || want.FsType == ignore) &&
 | 
			
		||||
		(e.Source == want.Source || want.Source == ignore) &&
 | 
			
		||||
		(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *MountInfoEntry) String() string {
 | 
			
		||||
	return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s",
 | 
			
		||||
		e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr,
 | 
			
		||||
		strings.Join(append(e.OptFields, "-"), " "), e.FsType, e.Source, e.FsOptstr)
 | 
			
		||||
}
 | 
			
		||||
@ -1,404 +0,0 @@
 | 
			
		||||
package vfs_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"path"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMountInfo(t *testing.T) {
 | 
			
		||||
	testCases := []mountInfoTest{
 | 
			
		||||
		{"count", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53/ /mnt/test rw,relatime - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoFields, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"sep", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoSep, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"id", sampleMountinfoBase + `
 | 
			
		||||
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			strconv.ErrSyntax, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"parent", sampleMountinfoBase + `
 | 
			
		||||
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			strconv.ErrSyntax, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"devno", sampleMountinfoBase + `
 | 
			
		||||
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			nil, "unexpected EOF", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"maj", sampleMountinfoBase + `
 | 
			
		||||
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			nil, "expected integer", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"min", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			nil, "expected integer", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"mountroot", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53  /mnt/test rw,relatime - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoEmpty, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"target", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53 /  rw,relatime - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoEmpty, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"vfs options", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53 / /mnt/test  - tmpfs  rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoEmpty, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"FS type", sampleMountinfoBase + `
 | 
			
		||||
21 20 0:53 / /mnt/test rw,relatime -   rw
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
 | 
			
		||||
			vfs.ErrMountInfoEmpty, "", nil, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
 | 
			
		||||
			m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NODIRATIME, []string{"meow"}),
 | 
			
		||||
		},
 | 
			
		||||
			mn(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", false,
 | 
			
		||||
				mn(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", false, nil,
 | 
			
		||||
					mn(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", false, nil,
 | 
			
		||||
						mn(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", false,
 | 
			
		||||
							mn(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", false, nil,
 | 
			
		||||
								mn(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", false, nil, nil)),
 | 
			
		||||
							nil))), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
 | 
			
		||||
				return []*vfs.MountInfoNode{
 | 
			
		||||
					n,
 | 
			
		||||
					n.FirstChild,
 | 
			
		||||
					n.FirstChild.NextSibling,
 | 
			
		||||
					n.FirstChild.NextSibling.NextSibling,
 | 
			
		||||
					n.FirstChild.NextSibling.NextSibling.FirstChild,
 | 
			
		||||
					n.FirstChild.NextSibling.NextSibling.FirstChild.NextSibling,
 | 
			
		||||
				}
 | 
			
		||||
			}},
 | 
			
		||||
 | 
			
		||||
		{"sample", sampleMountinfo, nil, "", []*wantMountInfo{
 | 
			
		||||
			m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
 | 
			
		||||
			m(21, 16, 0, 17, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", o(), "tmpfs", "tmpfs", "rw,mode=755", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(22, 21, 0, 18, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(23, 21, 0, 19, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuset", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(24, 21, 0, 20, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,ns", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(25, 21, 0, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpu", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(26, 21, 0, 22, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuacct", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(27, 21, 0, 23, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,memory", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(28, 21, 0, 24, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,devices", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(29, 21, 0, 25, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,freezer", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(30, 21, 0, 26, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,net_cls", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(31, 21, 0, 27, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,blkio", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(32, 16, 0, 28, "/", "/sys/kernel/security", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(33, 17, 0, 29, "/", "/dev/hugepages", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(34, 16, 0, 30, "/", "/sys/kernel/debug", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(35, 15, 0, 31, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(36, 17, 0, 32, "/", "/dev/mqueue", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(37, 15, 0, 14, "/", "/proc/bus/usb", "rw,relatime", o(), "usbfs", "/proc/bus/usb", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(38, 33, 0, 33, "/", "/dev/hugepages", "rw,relatime", o(), "hugetlbfs", "hugetlbfs", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(39, 36, 0, 12, "/", "/dev/mqueue", "rw,relatime", o(), "mqueue", "mqueue", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(40, 20, 8, 6, "/", "/boot", "rw,noatime", o(), "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
 | 
			
		||||
			m(41, 20, 253, 0, "/", "/home/kzak", "rw,noatime", o(), "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered", syscall.MS_NOATIME, nil),
 | 
			
		||||
			m(42, 35, 0, 34, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "binfmt_misc", "none", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(43, 16, 0, 35, "/", "/sys/fs/fuse/connections", "rw,relatime", o(), "fusectl", "fusectl", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(44, 41, 0, 36, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", o(), "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(45, 20, 0, 37, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", o(), "rpc_pipefs", "sunrpc", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(47, 20, 0, 38, "/", "/mnt/sounds", "rw,relatime", o(), "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(49, 20, 0, 56, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", o("shared:323"), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME|vfs.MS_NOSYMFOLLOW, nil),
 | 
			
		||||
		}, nil, nil},
 | 
			
		||||
 | 
			
		||||
		{"sample nosrc", sampleMountinfoNoSrc, nil, "", []*wantMountInfo{
 | 
			
		||||
			m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
			m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
 | 
			
		||||
			m(21, 20, 0, 53, "/", "/mnt/test", "rw,relatime", o("shared:212"), "tmpfs", "", "rw", syscall.MS_RELATIME, nil),
 | 
			
		||||
		}, nil, nil},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			t.Run("decode", func(t *testing.T) {
 | 
			
		||||
				var got *vfs.MountInfo
 | 
			
		||||
				d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
 | 
			
		||||
				err := d.Decode(&got)
 | 
			
		||||
				tc.check(t, d, "Decode",
 | 
			
		||||
					func(yield func(*vfs.MountInfoEntry) bool) {
 | 
			
		||||
						for cur := got; cur != nil; cur = cur.Next {
 | 
			
		||||
							if !yield(&cur.MountInfoEntry) {
 | 
			
		||||
								return
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}, func() error { return err })
 | 
			
		||||
				t.Run("reuse", func(t *testing.T) {
 | 
			
		||||
					tc.check(t, d, "Entries",
 | 
			
		||||
						d.Entries(), d.Err)
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			t.Run("iter", func(t *testing.T) {
 | 
			
		||||
				d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
 | 
			
		||||
				tc.check(t, d, "Entries",
 | 
			
		||||
					d.Entries(), d.Err)
 | 
			
		||||
 | 
			
		||||
				t.Run("reuse", func(t *testing.T) {
 | 
			
		||||
					tc.check(t, d, "Entries",
 | 
			
		||||
						d.Entries(), d.Err)
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			t.Run("yield", func(t *testing.T) {
 | 
			
		||||
				d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
 | 
			
		||||
				v := false
 | 
			
		||||
				d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })
 | 
			
		||||
				d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false })
 | 
			
		||||
 | 
			
		||||
				tc.check(t, d, "Entries",
 | 
			
		||||
					d.Entries(), d.Err)
 | 
			
		||||
 | 
			
		||||
				t.Run("reuse", func(t *testing.T) {
 | 
			
		||||
					tc.check(t, d, "Entries",
 | 
			
		||||
						d.Entries(), d.Err)
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type mountInfoTest struct {
 | 
			
		||||
	name      string
 | 
			
		||||
	sample    string
 | 
			
		||||
	wantErr   error
 | 
			
		||||
	wantError string
 | 
			
		||||
	want      []*wantMountInfo
 | 
			
		||||
 | 
			
		||||
	wantNode     *vfs.MountInfoNode
 | 
			
		||||
	wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName string,
 | 
			
		||||
	got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) {
 | 
			
		||||
	i := 0
 | 
			
		||||
	for cur := range got {
 | 
			
		||||
		if i == len(tc.want) {
 | 
			
		||||
			if funcName != "Decode" && (tc.wantErr != nil || tc.wantError != "") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			t.Errorf("%s: got more than %d entries", funcName, len(tc.want))
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) {
 | 
			
		||||
			t.Errorf("%s: entry %d\ngot:  %#v\nwant: %#v",
 | 
			
		||||
				funcName, i, cur, tc.want[i])
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		flags, unmatched := cur.Flags()
 | 
			
		||||
		if flags != tc.want[i].flags {
 | 
			
		||||
			t.Errorf("Flags(%q): %#x, want %#x",
 | 
			
		||||
				cur.VfsOptstr, flags, tc.want[i].flags)
 | 
			
		||||
		}
 | 
			
		||||
		if !slices.Equal(unmatched, tc.want[i].unmatched) {
 | 
			
		||||
			t.Errorf("Flags(%q): unmatched = %#q, want %#q",
 | 
			
		||||
				cur.VfsOptstr, unmatched, tc.want[i].unmatched)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if i != len(tc.want) {
 | 
			
		||||
		t.Errorf("%s: got %d entries, want %d", funcName, i, len(tc.want))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if tc.wantErr == nil && tc.wantError == "" && tc.wantCollectF != nil {
 | 
			
		||||
		t.Run("unfold", func(t *testing.T) {
 | 
			
		||||
			n, err := d.Unfold("/")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("Unfold: error = %v", err)
 | 
			
		||||
			} else {
 | 
			
		||||
				t.Run("stop", func(t *testing.T) {
 | 
			
		||||
					v := false
 | 
			
		||||
					n.Collective()(func(node *vfs.MountInfoNode) bool { v = !v; return v })
 | 
			
		||||
				})
 | 
			
		||||
 | 
			
		||||
				if !reflect.DeepEqual(n, tc.wantNode) {
 | 
			
		||||
					t.Errorf("Unfold: %s, want %s",
 | 
			
		||||
						mustMarshal(n), mustMarshal(tc.wantNode))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				t.Run("collective", func(t *testing.T) {
 | 
			
		||||
					wantCollect := tc.wantCollectF(n)
 | 
			
		||||
					if gotCollect := slices.Collect(n.Collective()); !reflect.DeepEqual(gotCollect, wantCollect) {
 | 
			
		||||
						t.Errorf("Collective: \ngot  %#v\nwant %#v",
 | 
			
		||||
							gotCollect, wantCollect)
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	} else if tc.wantNode != nil || tc.wantCollectF != nil {
 | 
			
		||||
		panic("invalid test case")
 | 
			
		||||
	} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
 | 
			
		||||
		if tc.wantError == "" {
 | 
			
		||||
			t.Errorf("Unfold: error = %v, wantErr %v",
 | 
			
		||||
				err, tc.wantErr)
 | 
			
		||||
		} else if err != nil && err.Error() != tc.wantError {
 | 
			
		||||
			t.Errorf("Unfold: error = %q, wantError %q",
 | 
			
		||||
				err, tc.wantError)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := gotErr(); !errors.Is(err, tc.wantErr) {
 | 
			
		||||
		if tc.wantError == "" {
 | 
			
		||||
			t.Errorf("%s: error = %v, wantErr %v",
 | 
			
		||||
				funcName, err, tc.wantErr)
 | 
			
		||||
		} else if err != nil && err.Error() != tc.wantError {
 | 
			
		||||
			t.Errorf("%s: error = %q, wantError %q",
 | 
			
		||||
				funcName, err, tc.wantError)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mustMarshal(v any) string {
 | 
			
		||||
	p, err := json.Marshal(v)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return string(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type wantMountInfo struct {
 | 
			
		||||
	vfs.MountInfoEntry
 | 
			
		||||
	flags     uintptr
 | 
			
		||||
	unmatched []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func m(
 | 
			
		||||
	id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
 | 
			
		||||
	flags uintptr, unmatched []string,
 | 
			
		||||
) *wantMountInfo {
 | 
			
		||||
	return &wantMountInfo{
 | 
			
		||||
		vfs.MountInfoEntry{
 | 
			
		||||
			ID:        id,
 | 
			
		||||
			Parent:    parent,
 | 
			
		||||
			Devno:     vfs.DevT{maj, min},
 | 
			
		||||
			Root:      root,
 | 
			
		||||
			Target:    target,
 | 
			
		||||
			VfsOptstr: vfsOptstr,
 | 
			
		||||
			OptFields: optFields,
 | 
			
		||||
			FsType:    fsType,
 | 
			
		||||
			Source:    source,
 | 
			
		||||
			FsOptstr:  fsOptstr,
 | 
			
		||||
		}, flags, unmatched,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mn(
 | 
			
		||||
	id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
 | 
			
		||||
	covered bool, firstChild, nextSibling *vfs.MountInfoNode,
 | 
			
		||||
) *vfs.MountInfoNode {
 | 
			
		||||
	return &vfs.MountInfoNode{
 | 
			
		||||
		MountInfoEntry: &vfs.MountInfoEntry{
 | 
			
		||||
			ID:        id,
 | 
			
		||||
			Parent:    parent,
 | 
			
		||||
			Devno:     vfs.DevT{maj, min},
 | 
			
		||||
			Root:      root,
 | 
			
		||||
			Target:    target,
 | 
			
		||||
			VfsOptstr: vfsOptstr,
 | 
			
		||||
			OptFields: optFields,
 | 
			
		||||
			FsType:    fsType,
 | 
			
		||||
			Source:    source,
 | 
			
		||||
			FsOptstr:  fsOptstr,
 | 
			
		||||
		},
 | 
			
		||||
		FirstChild:  firstChild,
 | 
			
		||||
		NextSibling: nextSibling,
 | 
			
		||||
		Clean:       path.Clean(target),
 | 
			
		||||
		Covered:     covered,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func o(field ...string) []string {
 | 
			
		||||
	if field == nil {
 | 
			
		||||
		return []string{}
 | 
			
		||||
	}
 | 
			
		||||
	return field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	sampleMountinfoBase = `15 20 0:3 / /proc rw,relatime - proc /proc rw
 | 
			
		||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
 | 
			
		||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
 | 
			
		||||
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
 | 
			
		||||
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
 | 
			
		||||
20 1 8:4 / / ro,noatime,nodiratime,meow - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered`
 | 
			
		||||
 | 
			
		||||
	sampleMountinfo = `15 20 0:3 / /proc rw,relatime - proc /proc rw
 | 
			
		||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
 | 
			
		||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
 | 
			
		||||
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
 | 
			
		||||
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
 | 
			
		||||
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
 | 
			
		||||
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
 | 
			
		||||
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
 | 
			
		||||
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
 | 
			
		||||
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
 | 
			
		||||
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
 | 
			
		||||
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
 | 
			
		||||
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
 | 
			
		||||
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
 | 
			
		||||
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
 | 
			
		||||
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
 | 
			
		||||
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
 | 
			
		||||
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
 | 
			
		||||
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
 | 
			
		||||
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
 | 
			
		||||
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
 | 
			
		||||
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
 | 
			
		||||
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
 | 
			
		||||
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
 | 
			
		||||
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
 | 
			
		||||
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
 | 
			
		||||
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`
 | 
			
		||||
 | 
			
		||||
	sampleMountinfoNoSrc = `15 20 0:3 / /proc rw,relatime - proc /proc rw
 | 
			
		||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
 | 
			
		||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
 | 
			
		||||
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
 | 
			
		||||
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
 | 
			
		||||
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
 | 
			
		||||
21 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs  rw`
 | 
			
		||||
)
 | 
			
		||||
@ -1,107 +0,0 @@
 | 
			
		||||
package vfs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"iter"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
 | 
			
		||||
type MountInfoNode struct {
 | 
			
		||||
	*MountInfoEntry
 | 
			
		||||
	FirstChild  *MountInfoNode `json:"first_child"`
 | 
			
		||||
	NextSibling *MountInfoNode `json:"next_sibling"`
 | 
			
		||||
 | 
			
		||||
	Clean   string `json:"clean"`
 | 
			
		||||
	Covered bool   `json:"covered"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Collective returns an iterator over visible mountinfo nodes.
 | 
			
		||||
func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] {
 | 
			
		||||
	return func(yield func(*MountInfoNode) bool) { n.visit(yield) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool {
 | 
			
		||||
	if !n.Covered && !yield(n) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for cur := n.FirstChild; cur != nil; cur = cur.NextSibling {
 | 
			
		||||
		if !cur.visit(yield) {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unfold unfolds the mount hierarchy and resolves covered paths.
 | 
			
		||||
func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
 | 
			
		||||
	targetClean := path.Clean(target)
 | 
			
		||||
 | 
			
		||||
	var mountinfoSize int
 | 
			
		||||
	for range d.Entries() {
 | 
			
		||||
		mountinfoSize++
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.Err(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mountinfo := make([]*MountInfoNode, mountinfoSize)
 | 
			
		||||
	// mount ID to index lookup
 | 
			
		||||
	idIndex := make(map[int]int, mountinfoSize)
 | 
			
		||||
	// final entry to match target
 | 
			
		||||
	targetIndex := -1
 | 
			
		||||
	{
 | 
			
		||||
		i := 0
 | 
			
		||||
		for ent := range d.Entries() {
 | 
			
		||||
			mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent}
 | 
			
		||||
			idIndex[ent.ID] = i
 | 
			
		||||
			if mountinfo[i].Clean == targetClean {
 | 
			
		||||
				targetIndex = i
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if targetIndex == -1 {
 | 
			
		||||
		return nil, syscall.ESTALE
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, cur := range mountinfo {
 | 
			
		||||
		var parent *MountInfoNode
 | 
			
		||||
		if p, ok := idIndex[cur.Parent]; !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		} else {
 | 
			
		||||
			parent = mountinfo[p]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !strings.HasPrefix(cur.Clean, targetClean) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if parent.Clean == cur.Clean {
 | 
			
		||||
			parent.Covered = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		covered := false
 | 
			
		||||
		nsp := &parent.FirstChild
 | 
			
		||||
		for s := parent.FirstChild; s != nil; s = s.NextSibling {
 | 
			
		||||
			if strings.HasPrefix(cur.Clean, s.Clean) {
 | 
			
		||||
				covered = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if strings.HasPrefix(s.Clean, cur.Clean) {
 | 
			
		||||
				*nsp = s.NextSibling
 | 
			
		||||
			} else {
 | 
			
		||||
				nsp = &s.NextSibling
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if covered {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		*nsp = cur
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return mountinfo[targetIndex], nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,93 +0,0 @@
 | 
			
		||||
package vfs_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestUnfold(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		sample  string
 | 
			
		||||
		target  string
 | 
			
		||||
		wantErr error
 | 
			
		||||
 | 
			
		||||
		want         *vfs.MountInfoNode
 | 
			
		||||
		wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
 | 
			
		||||
		wantCollectN []string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"no match",
 | 
			
		||||
			sampleMountinfoBase,
 | 
			
		||||
			"/mnt",
 | 
			
		||||
			syscall.ESTALE, nil, nil, nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"cover",
 | 
			
		||||
			`33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
 | 
			
		||||
37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw
 | 
			
		||||
551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw
 | 
			
		||||
595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw
 | 
			
		||||
611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw
 | 
			
		||||
625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw
 | 
			
		||||
641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
 | 
			
		||||
644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
 | 
			
		||||
`, "/mnt", nil,
 | 
			
		||||
			mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false,
 | 
			
		||||
				mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false,
 | 
			
		||||
					mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true,
 | 
			
		||||
						mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true,
 | 
			
		||||
							mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false,
 | 
			
		||||
								nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
 | 
			
		||||
				return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild}
 | 
			
		||||
			}, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
 | 
			
		||||
			got, err := d.Unfold(tc.target)
 | 
			
		||||
 | 
			
		||||
			if !errors.Is(err, tc.wantErr) {
 | 
			
		||||
				t.Errorf("Unfold: error = %v, wantErr %v",
 | 
			
		||||
					err, tc.wantErr)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !reflect.DeepEqual(got, tc.want) {
 | 
			
		||||
				t.Errorf("Unfold:\ngot  %s\nwant %s",
 | 
			
		||||
					mustMarshal(got), mustMarshal(tc.want))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err == nil && tc.wantCollectF != nil {
 | 
			
		||||
				t.Run("collective", func(t *testing.T) {
 | 
			
		||||
					wantCollect := tc.wantCollectF(got)
 | 
			
		||||
					gotCollect := slices.Collect(got.Collective())
 | 
			
		||||
					if !reflect.DeepEqual(gotCollect, wantCollect) {
 | 
			
		||||
						t.Errorf("Collective: \ngot  %#v\nwant %#v",
 | 
			
		||||
							gotCollect, wantCollect)
 | 
			
		||||
					}
 | 
			
		||||
					t.Run("target", func(t *testing.T) {
 | 
			
		||||
						gotCollectN := slices.Collect[string](func(yield func(v string) bool) {
 | 
			
		||||
							for _, cur := range gotCollect {
 | 
			
		||||
								if !yield(cur.Clean) {
 | 
			
		||||
									return
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						})
 | 
			
		||||
						if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) {
 | 
			
		||||
							t.Errorf("Collective: got %q, want %q",
 | 
			
		||||
								gotCollectN, tc.wantCollectN)
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -4,12 +4,6 @@
 | 
			
		||||
  config,
 | 
			
		||||
  ...
 | 
			
		||||
}:
 | 
			
		||||
let
 | 
			
		||||
  testCases = import ./sandbox/case {
 | 
			
		||||
    inherit (pkgs) lib callPackage foot;
 | 
			
		||||
    inherit (config.environment.fortify.package) version;
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
{
 | 
			
		||||
  users.users = {
 | 
			
		||||
    alice = {
 | 
			
		||||
@ -108,10 +102,21 @@ in
 | 
			
		||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
			
		||||
 | 
			
		||||
    apps = [
 | 
			
		||||
      testCases.preset
 | 
			
		||||
      testCases.tty
 | 
			
		||||
      testCases.mapuid
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        name = "check-sandbox";
 | 
			
		||||
        verbose = true;
 | 
			
		||||
        share = pkgs.foot;
 | 
			
		||||
        packages = [ ];
 | 
			
		||||
        command = "${pkgs.callPackage ./sandbox {
 | 
			
		||||
          inherit (config.environment.fortify.package) version;
 | 
			
		||||
        }}";
 | 
			
		||||
        extraPaths = [
 | 
			
		||||
          {
 | 
			
		||||
            src = "/proc/mounts";
 | 
			
		||||
            dst = "/.fortify/host-mounts";
 | 
			
		||||
          }
 | 
			
		||||
        ];
 | 
			
		||||
      }
 | 
			
		||||
      {
 | 
			
		||||
        name = "ne-foot";
 | 
			
		||||
        verbose = true;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  nixosTest,
 | 
			
		||||
  buildFHSEnv,
 | 
			
		||||
  writeShellScriptBin,
 | 
			
		||||
 | 
			
		||||
  system,
 | 
			
		||||
@ -13,21 +12,6 @@ nixosTest {
 | 
			
		||||
  name = "fortify" + (if withRace then "-race" else "");
 | 
			
		||||
  nodes.machine =
 | 
			
		||||
    { options, pkgs, ... }:
 | 
			
		||||
    let
 | 
			
		||||
      fhs =
 | 
			
		||||
        let
 | 
			
		||||
          fortify = options.environment.fortify.package.default;
 | 
			
		||||
        in
 | 
			
		||||
        buildFHSEnv {
 | 
			
		||||
          pname = "fortify-fhs";
 | 
			
		||||
          inherit (fortify) version;
 | 
			
		||||
          targetPkgs = _: fortify.targetPkgs;
 | 
			
		||||
          extraOutputsToInstall = [ "dev" ];
 | 
			
		||||
          profile = ''
 | 
			
		||||
            export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
 | 
			
		||||
          '';
 | 
			
		||||
        };
 | 
			
		||||
    in
 | 
			
		||||
    {
 | 
			
		||||
      environment.systemPackages = [
 | 
			
		||||
        # For go tests:
 | 
			
		||||
@ -37,7 +21,7 @@ nixosTest {
 | 
			
		||||
          cp -r "${self.packages.${system}.fortify.src}" "$WORK"
 | 
			
		||||
          chmod -R +w "$WORK"
 | 
			
		||||
          cd "$WORK"
 | 
			
		||||
          ${fhs}/bin/fortify-fhs -c \
 | 
			
		||||
          ${self.packages.${system}.fhs}/bin/fortify-fhs -c \
 | 
			
		||||
            'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
 | 
			
		||||
        '')
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,3 @@
 | 
			
		||||
/*
 | 
			
		||||
Package sandbox provides utilities for checking sandbox outcome.
 | 
			
		||||
 | 
			
		||||
This package must never be used outside integration tests, there is a much better native implementation of mountinfo
 | 
			
		||||
in the public sandbox/vfs package. Files in this package are excluded by the build system to prevent accidental misuse.
 | 
			
		||||
*/
 | 
			
		||||
package sandbox
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@ -22,89 +16,76 @@ var (
 | 
			
		||||
func printf(format string, v ...any) { printfFunc(format, v...) }
 | 
			
		||||
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
 | 
			
		||||
 | 
			
		||||
type TestCase struct {
 | 
			
		||||
	FS      *FS               `json:"fs"`
 | 
			
		||||
	Mount   []*MountinfoEntry `json:"mount"`
 | 
			
		||||
	Seccomp bool              `json:"seccomp"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type T struct {
 | 
			
		||||
	FS fs.FS
 | 
			
		||||
 | 
			
		||||
	MountsPath string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *T) MustCheckFile(wantFilePath string) {
 | 
			
		||||
	var want *TestCase
 | 
			
		||||
	mustDecode(wantFilePath, &want)
 | 
			
		||||
	t.MustCheck(want)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *T) MustCheck(want *TestCase) {
 | 
			
		||||
	if want.FS != nil && t.FS != nil {
 | 
			
		||||
		if err := want.FS.Compare(".", t.FS); err != nil {
 | 
			
		||||
			fatalf("%v", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		printf("[SKIP] skipping fs check")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if want.Mount != nil {
 | 
			
		||||
		var fail bool
 | 
			
		||||
		m := mustParseMountinfo(t.MountsPath)
 | 
			
		||||
		i := 0
 | 
			
		||||
		for ent := range m.Entries() {
 | 
			
		||||
			if i == len(want.Mount) {
 | 
			
		||||
				fatalf("got more than %d entries", i)
 | 
			
		||||
			}
 | 
			
		||||
			if !ent.EqualWithIgnore(want.Mount[i], "//ignore") {
 | 
			
		||||
				fail = true
 | 
			
		||||
				printf("[FAIL] %s", ent)
 | 
			
		||||
			} else {
 | 
			
		||||
				printf("[ OK ] %s", ent)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i++
 | 
			
		||||
		}
 | 
			
		||||
		if err := m.Err(); err != nil {
 | 
			
		||||
			fatalf("%v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if i != len(want.Mount) {
 | 
			
		||||
			fatalf("got %d entries, want %d", i, len(want.Mount))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fail {
 | 
			
		||||
			fatalf("[FAIL] some mount points did not match")
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		printf("[SKIP] skipping mounts check")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if want.Seccomp {
 | 
			
		||||
		if TrySyscalls() != nil {
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		printf("[SKIP] skipping seccomp check")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mustDecode(wantFilePath string, v any) {
 | 
			
		||||
	if f, err := os.Open(wantFilePath); err != nil {
 | 
			
		||||
		fatalf("cannot open %q: %v", wantFilePath, err)
 | 
			
		||||
func mustDecode(wantFile string, v any) {
 | 
			
		||||
	if f, err := os.Open(wantFile); err != nil {
 | 
			
		||||
		fatalf("cannot open %q: %v", wantFile, err)
 | 
			
		||||
	} else if err = json.NewDecoder(f).Decode(v); err != nil {
 | 
			
		||||
		fatalf("cannot decode %q: %v", wantFilePath, err)
 | 
			
		||||
		fatalf("cannot decode %q: %v", wantFile, err)
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		fatalf("cannot close %q: %v", wantFilePath, err)
 | 
			
		||||
		fatalf("cannot close %q: %v", wantFile, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mustParseMountinfo(name string) *Mountinfo {
 | 
			
		||||
	m := NewMountinfo(name)
 | 
			
		||||
	if err := m.Parse(); err != nil {
 | 
			
		||||
		fatalf("%v", err)
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
func MustAssertMounts(name, hostMountsFile, wantFile string) {
 | 
			
		||||
	hostMounts := make([]*Mntent, 0, 128)
 | 
			
		||||
	if err := IterMounts(hostMountsFile, func(e *Mntent) {
 | 
			
		||||
		hostMounts = append(hostMounts, e)
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fatalf("cannot parse host mounts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var want []Mntent
 | 
			
		||||
	mustDecode(wantFile, &want)
 | 
			
		||||
 | 
			
		||||
	for i := range want {
 | 
			
		||||
		if want[i].Opts == "host_passthrough" {
 | 
			
		||||
			for _, ent := range hostMounts {
 | 
			
		||||
				if want[i].FSName == ent.FSName {
 | 
			
		||||
					// special case for tmpfs bind mounts
 | 
			
		||||
					if want[i].FSName == "tmpfs" && want[i].Dir != ent.Dir {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					want[i].Opts = ent.Opts
 | 
			
		||||
					goto out
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			fatalf("host passthrough missing %q", want[i].FSName)
 | 
			
		||||
		out:
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i := 0
 | 
			
		||||
	if err := IterMounts(name, func(e *Mntent) {
 | 
			
		||||
		if i == len(want) {
 | 
			
		||||
			fatalf("got more than %d entries", i)
 | 
			
		||||
		}
 | 
			
		||||
		if !e.Is(&want[i]) {
 | 
			
		||||
			fatalf("entry %d\n got: %s\nwant: %s", i,
 | 
			
		||||
				e, &want[i])
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		printf("%s", e)
 | 
			
		||||
		i++
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fatalf("cannot iterate mounts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustAssertFS(e fs.FS, wantFile string) {
 | 
			
		||||
	var want *FS
 | 
			
		||||
	mustDecode(wantFile, &want)
 | 
			
		||||
	if want == nil {
 | 
			
		||||
		fatalf("invalid payload")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := want.Compare(".", e); err != nil {
 | 
			
		||||
		fatalf("%v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustAssertSeccomp() {
 | 
			
		||||
	if TrySyscalls() != nil {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	return m
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  writeText,
 | 
			
		||||
  buildGoModule,
 | 
			
		||||
  pkg-config,
 | 
			
		||||
  util-linux,
 | 
			
		||||
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
buildGoModule {
 | 
			
		||||
  pname = "check-sandbox";
 | 
			
		||||
  inherit version;
 | 
			
		||||
 | 
			
		||||
  src = ../.;
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
  buildInputs = [ util-linux ];
 | 
			
		||||
  nativeBuildInputs = [ pkg-config ];
 | 
			
		||||
 | 
			
		||||
  preBuild = ''
 | 
			
		||||
    go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
 | 
			
		||||
    cp ${writeText "main.go" ''
 | 
			
		||||
      package main
 | 
			
		||||
 | 
			
		||||
      import "os"
 | 
			
		||||
      import "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
 | 
			
		||||
      func main() { (&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1]) }
 | 
			
		||||
    ''} main.go
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  lib,
 | 
			
		||||
  callPackage,
 | 
			
		||||
  foot,
 | 
			
		||||
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
let
 | 
			
		||||
  fs = mode: dir: data: {
 | 
			
		||||
    mode = lib.fromHexString mode;
 | 
			
		||||
    inherit
 | 
			
		||||
      dir
 | 
			
		||||
      data
 | 
			
		||||
      ;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  ignore = "//ignore";
 | 
			
		||||
 | 
			
		||||
  ent = root: target: vfs_optstr: fstype: source: fs_optstr: {
 | 
			
		||||
    id = -1;
 | 
			
		||||
    parent = -1;
 | 
			
		||||
    inherit
 | 
			
		||||
      root
 | 
			
		||||
      target
 | 
			
		||||
      vfs_optstr
 | 
			
		||||
      fstype
 | 
			
		||||
      source
 | 
			
		||||
      fs_optstr
 | 
			
		||||
      ;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  checkSandbox = callPackage ../. { inherit version; };
 | 
			
		||||
 | 
			
		||||
  callTestCase =
 | 
			
		||||
    path:
 | 
			
		||||
    let
 | 
			
		||||
      tc = import path {
 | 
			
		||||
        inherit
 | 
			
		||||
          fs
 | 
			
		||||
          ent
 | 
			
		||||
          ignore
 | 
			
		||||
          ;
 | 
			
		||||
      };
 | 
			
		||||
    in
 | 
			
		||||
    {
 | 
			
		||||
      name = "check-sandbox-${tc.name}";
 | 
			
		||||
      verbose = true;
 | 
			
		||||
      inherit (tc) tty mapRealUid;
 | 
			
		||||
      share = foot;
 | 
			
		||||
      packages = [ ];
 | 
			
		||||
      command = builtins.toString (checkSandbox tc.name tc.want);
 | 
			
		||||
    };
 | 
			
		||||
in
 | 
			
		||||
{
 | 
			
		||||
  preset = callTestCase ./preset.nix;
 | 
			
		||||
  tty = callTestCase ./tty.nix;
 | 
			
		||||
  mapuid = callTestCase ./mapuid.nix;
 | 
			
		||||
}
 | 
			
		||||
@ -1,221 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  fs,
 | 
			
		||||
  ent,
 | 
			
		||||
  ignore,
 | 
			
		||||
}:
 | 
			
		||||
{
 | 
			
		||||
  name = "mapuid";
 | 
			
		||||
  tty = false;
 | 
			
		||||
  mapRealUid = true;
 | 
			
		||||
 | 
			
		||||
  want = {
 | 
			
		||||
    fs = fs "dead" {
 | 
			
		||||
      ".fortify" = fs "800001ed" {
 | 
			
		||||
        etc = fs "800001ed" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
 | 
			
		||||
      dev = fs "800001ed" {
 | 
			
		||||
        core = fs "80001ff" null null;
 | 
			
		||||
        dri = fs "800001ed" {
 | 
			
		||||
          by-path = fs "800001ed" {
 | 
			
		||||
            "pci-0000:00:09.0-card" = fs "80001ff" null null;
 | 
			
		||||
            "pci-0000:00:09.0-render" = fs "80001ff" null null;
 | 
			
		||||
          } null;
 | 
			
		||||
          card0 = fs "42001b0" null null;
 | 
			
		||||
          renderD128 = fs "42001b6" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        fd = fs "80001ff" null null;
 | 
			
		||||
        full = fs "42001b6" null null;
 | 
			
		||||
        mqueue = fs "801001ff" { } null;
 | 
			
		||||
        null = fs "42001b6" null "";
 | 
			
		||||
        ptmx = fs "80001ff" null null;
 | 
			
		||||
        pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
 | 
			
		||||
        random = fs "42001b6" null null;
 | 
			
		||||
        shm = fs "800001ed" { } null;
 | 
			
		||||
        stderr = fs "80001ff" null null;
 | 
			
		||||
        stdin = fs "80001ff" null null;
 | 
			
		||||
        stdout = fs "80001ff" null null;
 | 
			
		||||
        tty = fs "42001b6" null null;
 | 
			
		||||
        urandom = fs "42001b6" null null;
 | 
			
		||||
        zero = fs "42001b6" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      etc = fs "800001c0" {
 | 
			
		||||
        ".clean" = fs "80001ff" null null;
 | 
			
		||||
        ".updated" = fs "80001ff" null null;
 | 
			
		||||
        "NIXOS" = fs "80001ff" null null;
 | 
			
		||||
        "X11" = fs "80001ff" null null;
 | 
			
		||||
        "alsa" = fs "80001ff" null null;
 | 
			
		||||
        "bashrc" = fs "80001ff" null null;
 | 
			
		||||
        "binfmt.d" = fs "80001ff" null null;
 | 
			
		||||
        "dbus-1" = fs "80001ff" null null;
 | 
			
		||||
        "default" = fs "80001ff" null null;
 | 
			
		||||
        "dhcpcd.exit-hook" = fs "80001ff" null null;
 | 
			
		||||
        "fonts" = fs "80001ff" null null;
 | 
			
		||||
        "fstab" = fs "80001ff" null null;
 | 
			
		||||
        "fsurc" = fs "80001ff" null null;
 | 
			
		||||
        "fuse.conf" = fs "80001ff" null null;
 | 
			
		||||
        "group" = fs "180" null "fortify:x:100:\n";
 | 
			
		||||
        "host.conf" = fs "80001ff" null null;
 | 
			
		||||
        "hostname" = fs "80001ff" null null;
 | 
			
		||||
        "hosts" = fs "80001ff" null null;
 | 
			
		||||
        "inputrc" = fs "80001ff" null null;
 | 
			
		||||
        "issue" = fs "80001ff" null null;
 | 
			
		||||
        "kbd" = fs "80001ff" null null;
 | 
			
		||||
        "locale.conf" = fs "80001ff" null null;
 | 
			
		||||
        "login.defs" = fs "80001ff" null null;
 | 
			
		||||
        "lsb-release" = fs "80001ff" null null;
 | 
			
		||||
        "lvm" = fs "80001ff" null null;
 | 
			
		||||
        "machine-id" = fs "80001ff" null null;
 | 
			
		||||
        "man_db.conf" = fs "80001ff" null null;
 | 
			
		||||
        "modprobe.d" = fs "80001ff" null null;
 | 
			
		||||
        "modules-load.d" = fs "80001ff" null null;
 | 
			
		||||
        "mtab" = fs "80001ff" null null;
 | 
			
		||||
        "nanorc" = fs "80001ff" null null;
 | 
			
		||||
        "netgroup" = fs "80001ff" null null;
 | 
			
		||||
        "nix" = fs "80001ff" null null;
 | 
			
		||||
        "nixos" = fs "80001ff" null null;
 | 
			
		||||
        "nscd.conf" = fs "80001ff" null null;
 | 
			
		||||
        "nsswitch.conf" = fs "80001ff" null null;
 | 
			
		||||
        "os-release" = fs "80001ff" null null;
 | 
			
		||||
        "pam" = fs "80001ff" null null;
 | 
			
		||||
        "pam.d" = fs "80001ff" null null;
 | 
			
		||||
        "passwd" = fs "180" null "u0_a3:x:1000:100:Fortify:/var/lib/fortify/u0/a3:/run/current-system/sw/bin/bash\n";
 | 
			
		||||
        "pipewire" = fs "80001ff" null null;
 | 
			
		||||
        "pki" = fs "80001ff" null null;
 | 
			
		||||
        "polkit-1" = fs "80001ff" null null;
 | 
			
		||||
        "profile" = fs "80001ff" null null;
 | 
			
		||||
        "profiles" = fs "80001ff" null null;
 | 
			
		||||
        "protocols" = fs "80001ff" null null;
 | 
			
		||||
        "resolv.conf" = fs "80001ff" null null;
 | 
			
		||||
        "resolvconf.conf" = fs "80001ff" null null;
 | 
			
		||||
        "rpc" = fs "80001ff" null null;
 | 
			
		||||
        "services" = fs "80001ff" null null;
 | 
			
		||||
        "set-environment" = fs "80001ff" null null;
 | 
			
		||||
        "shadow" = fs "80001ff" null null;
 | 
			
		||||
        "shells" = fs "80001ff" null null;
 | 
			
		||||
        "ssh" = fs "80001ff" null null;
 | 
			
		||||
        "ssl" = fs "80001ff" null null;
 | 
			
		||||
        "static" = fs "80001ff" null null;
 | 
			
		||||
        "subgid" = fs "80001ff" null null;
 | 
			
		||||
        "subuid" = fs "80001ff" null null;
 | 
			
		||||
        "sudoers" = fs "80001ff" null null;
 | 
			
		||||
        "sway" = fs "80001ff" null null;
 | 
			
		||||
        "sysctl.d" = fs "80001ff" null null;
 | 
			
		||||
        "systemd" = fs "80001ff" null null;
 | 
			
		||||
        "terminfo" = fs "80001ff" null null;
 | 
			
		||||
        "tmpfiles.d" = fs "80001ff" null null;
 | 
			
		||||
        "udev" = fs "80001ff" null null;
 | 
			
		||||
        "vconsole.conf" = fs "80001ff" null null;
 | 
			
		||||
        "xdg" = fs "80001ff" null null;
 | 
			
		||||
        "zoneinfo" = fs "80001ff" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      nix = fs "800001c0" { store = fs "801001fd" null null; } null;
 | 
			
		||||
      proc = fs "8000016d" null null;
 | 
			
		||||
      run = fs "800001c0" {
 | 
			
		||||
        current-system = fs "8000016d" null null;
 | 
			
		||||
        opengl-driver = fs "8000016d" null null;
 | 
			
		||||
        user = fs "800001ed" {
 | 
			
		||||
          "1000" = fs "800001ed" {
 | 
			
		||||
            bus = fs "10001fd" null null;
 | 
			
		||||
            pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
 | 
			
		||||
            wayland-0 = fs "1000038" null null;
 | 
			
		||||
          } null;
 | 
			
		||||
        } null;
 | 
			
		||||
      } null;
 | 
			
		||||
      sys = fs "800001c0" {
 | 
			
		||||
        block = fs "800001ed" {
 | 
			
		||||
          fd0 = fs "80001ff" null null;
 | 
			
		||||
          loop0 = fs "80001ff" null null;
 | 
			
		||||
          loop1 = fs "80001ff" null null;
 | 
			
		||||
          loop2 = fs "80001ff" null null;
 | 
			
		||||
          loop3 = fs "80001ff" null null;
 | 
			
		||||
          loop4 = fs "80001ff" null null;
 | 
			
		||||
          loop5 = fs "80001ff" null null;
 | 
			
		||||
          loop6 = fs "80001ff" null null;
 | 
			
		||||
          loop7 = fs "80001ff" null null;
 | 
			
		||||
          sr0 = fs "80001ff" null null;
 | 
			
		||||
          vda = fs "80001ff" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        bus = fs "800001ed" null null;
 | 
			
		||||
        class = fs "800001ed" null null;
 | 
			
		||||
        dev = fs "800001ed" {
 | 
			
		||||
          block = fs "800001ed" null null;
 | 
			
		||||
          char = fs "800001ed" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        devices = fs "800001ed" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      tmp = fs "800001f8" { } null;
 | 
			
		||||
      usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
 | 
			
		||||
      var = fs "800001c0" {
 | 
			
		||||
        lib = fs "800001c0" {
 | 
			
		||||
          fortify = fs "800001c0" {
 | 
			
		||||
            u0 = fs "800001c0" {
 | 
			
		||||
              a3 = fs "800001c0" {
 | 
			
		||||
                ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
 | 
			
		||||
                ".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
 | 
			
		||||
                ".local" = fs "800001ed" {
 | 
			
		||||
                  state = fs "800001ed" {
 | 
			
		||||
                    home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
 | 
			
		||||
                    nix = fs "800001ed" {
 | 
			
		||||
                      profiles = fs "800001ed" {
 | 
			
		||||
                        home-manager = fs "80001ff" null null;
 | 
			
		||||
                        home-manager-1-link = fs "80001ff" null null;
 | 
			
		||||
                        profile = fs "80001ff" null null;
 | 
			
		||||
                        profile-1-link = fs "80001ff" null null;
 | 
			
		||||
                      } null;
 | 
			
		||||
                    } null;
 | 
			
		||||
                  } null;
 | 
			
		||||
                } null;
 | 
			
		||||
                ".nix-defexpr" = fs "800001ed" {
 | 
			
		||||
                  channels = fs "80001ff" null null;
 | 
			
		||||
                  channels_root = fs "80001ff" null null;
 | 
			
		||||
                } null;
 | 
			
		||||
                ".nix-profile" = fs "80001ff" null null;
 | 
			
		||||
              } null;
 | 
			
		||||
            } null;
 | 
			
		||||
          } null;
 | 
			
		||||
        } null;
 | 
			
		||||
        run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
 | 
			
		||||
      } null;
 | 
			
		||||
    } null;
 | 
			
		||||
 | 
			
		||||
    mount = [
 | 
			
		||||
      (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
 | 
			
		||||
      (ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
 | 
			
		||||
      (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
 | 
			
		||||
      (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent "/tmp/fortify.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/var/lib/fortify/u0/a3" "/var/lib/fortify/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
 | 
			
		||||
      (ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
 | 
			
		||||
      (ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000003,gid=1000003")
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    seccomp = true;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -1,223 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  fs,
 | 
			
		||||
  ent,
 | 
			
		||||
  ignore,
 | 
			
		||||
}:
 | 
			
		||||
{
 | 
			
		||||
  name = "tty";
 | 
			
		||||
  tty = true;
 | 
			
		||||
  mapRealUid = false;
 | 
			
		||||
 | 
			
		||||
  want = {
 | 
			
		||||
    fs = fs "dead" {
 | 
			
		||||
      ".fortify" = fs "800001ed" {
 | 
			
		||||
        etc = fs "800001ed" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
 | 
			
		||||
      dev = fs "800001ed" {
 | 
			
		||||
        console = fs "4200190" null null;
 | 
			
		||||
        core = fs "80001ff" null null;
 | 
			
		||||
        dri = fs "800001ed" {
 | 
			
		||||
          by-path = fs "800001ed" {
 | 
			
		||||
            "pci-0000:00:09.0-card" = fs "80001ff" null null;
 | 
			
		||||
            "pci-0000:00:09.0-render" = fs "80001ff" null null;
 | 
			
		||||
          } null;
 | 
			
		||||
          card0 = fs "42001b0" null null;
 | 
			
		||||
          renderD128 = fs "42001b6" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        fd = fs "80001ff" null null;
 | 
			
		||||
        full = fs "42001b6" null null;
 | 
			
		||||
        mqueue = fs "801001ff" { } null;
 | 
			
		||||
        null = fs "42001b6" null "";
 | 
			
		||||
        ptmx = fs "80001ff" null null;
 | 
			
		||||
        pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
 | 
			
		||||
        random = fs "42001b6" null null;
 | 
			
		||||
        shm = fs "800001ed" { } null;
 | 
			
		||||
        stderr = fs "80001ff" null null;
 | 
			
		||||
        stdin = fs "80001ff" null null;
 | 
			
		||||
        stdout = fs "80001ff" null null;
 | 
			
		||||
        tty = fs "42001b6" null null;
 | 
			
		||||
        urandom = fs "42001b6" null null;
 | 
			
		||||
        zero = fs "42001b6" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      etc = fs "800001c0" {
 | 
			
		||||
        ".clean" = fs "80001ff" null null;
 | 
			
		||||
        ".updated" = fs "80001ff" null null;
 | 
			
		||||
        "NIXOS" = fs "80001ff" null null;
 | 
			
		||||
        "X11" = fs "80001ff" null null;
 | 
			
		||||
        "alsa" = fs "80001ff" null null;
 | 
			
		||||
        "bashrc" = fs "80001ff" null null;
 | 
			
		||||
        "binfmt.d" = fs "80001ff" null null;
 | 
			
		||||
        "dbus-1" = fs "80001ff" null null;
 | 
			
		||||
        "default" = fs "80001ff" null null;
 | 
			
		||||
        "dhcpcd.exit-hook" = fs "80001ff" null null;
 | 
			
		||||
        "fonts" = fs "80001ff" null null;
 | 
			
		||||
        "fstab" = fs "80001ff" null null;
 | 
			
		||||
        "fsurc" = fs "80001ff" null null;
 | 
			
		||||
        "fuse.conf" = fs "80001ff" null null;
 | 
			
		||||
        "group" = fs "180" null "fortify:x:65534:\n";
 | 
			
		||||
        "host.conf" = fs "80001ff" null null;
 | 
			
		||||
        "hostname" = fs "80001ff" null null;
 | 
			
		||||
        "hosts" = fs "80001ff" null null;
 | 
			
		||||
        "inputrc" = fs "80001ff" null null;
 | 
			
		||||
        "issue" = fs "80001ff" null null;
 | 
			
		||||
        "kbd" = fs "80001ff" null null;
 | 
			
		||||
        "locale.conf" = fs "80001ff" null null;
 | 
			
		||||
        "login.defs" = fs "80001ff" null null;
 | 
			
		||||
        "lsb-release" = fs "80001ff" null null;
 | 
			
		||||
        "lvm" = fs "80001ff" null null;
 | 
			
		||||
        "machine-id" = fs "80001ff" null null;
 | 
			
		||||
        "man_db.conf" = fs "80001ff" null null;
 | 
			
		||||
        "modprobe.d" = fs "80001ff" null null;
 | 
			
		||||
        "modules-load.d" = fs "80001ff" null null;
 | 
			
		||||
        "mtab" = fs "80001ff" null null;
 | 
			
		||||
        "nanorc" = fs "80001ff" null null;
 | 
			
		||||
        "netgroup" = fs "80001ff" null null;
 | 
			
		||||
        "nix" = fs "80001ff" null null;
 | 
			
		||||
        "nixos" = fs "80001ff" null null;
 | 
			
		||||
        "nscd.conf" = fs "80001ff" null null;
 | 
			
		||||
        "nsswitch.conf" = fs "80001ff" null null;
 | 
			
		||||
        "os-release" = fs "80001ff" null null;
 | 
			
		||||
        "pam" = fs "80001ff" null null;
 | 
			
		||||
        "pam.d" = fs "80001ff" null null;
 | 
			
		||||
        "passwd" = fs "180" null "u0_a2:x:65534:65534:Fortify:/var/lib/fortify/u0/a2:/run/current-system/sw/bin/bash\n";
 | 
			
		||||
        "pipewire" = fs "80001ff" null null;
 | 
			
		||||
        "pki" = fs "80001ff" null null;
 | 
			
		||||
        "polkit-1" = fs "80001ff" null null;
 | 
			
		||||
        "profile" = fs "80001ff" null null;
 | 
			
		||||
        "profiles" = fs "80001ff" null null;
 | 
			
		||||
        "protocols" = fs "80001ff" null null;
 | 
			
		||||
        "resolv.conf" = fs "80001ff" null null;
 | 
			
		||||
        "resolvconf.conf" = fs "80001ff" null null;
 | 
			
		||||
        "rpc" = fs "80001ff" null null;
 | 
			
		||||
        "services" = fs "80001ff" null null;
 | 
			
		||||
        "set-environment" = fs "80001ff" null null;
 | 
			
		||||
        "shadow" = fs "80001ff" null null;
 | 
			
		||||
        "shells" = fs "80001ff" null null;
 | 
			
		||||
        "ssh" = fs "80001ff" null null;
 | 
			
		||||
        "ssl" = fs "80001ff" null null;
 | 
			
		||||
        "static" = fs "80001ff" null null;
 | 
			
		||||
        "subgid" = fs "80001ff" null null;
 | 
			
		||||
        "subuid" = fs "80001ff" null null;
 | 
			
		||||
        "sudoers" = fs "80001ff" null null;
 | 
			
		||||
        "sway" = fs "80001ff" null null;
 | 
			
		||||
        "sysctl.d" = fs "80001ff" null null;
 | 
			
		||||
        "systemd" = fs "80001ff" null null;
 | 
			
		||||
        "terminfo" = fs "80001ff" null null;
 | 
			
		||||
        "tmpfiles.d" = fs "80001ff" null null;
 | 
			
		||||
        "udev" = fs "80001ff" null null;
 | 
			
		||||
        "vconsole.conf" = fs "80001ff" null null;
 | 
			
		||||
        "xdg" = fs "80001ff" null null;
 | 
			
		||||
        "zoneinfo" = fs "80001ff" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      nix = fs "800001c0" { store = fs "801001fd" null null; } null;
 | 
			
		||||
      proc = fs "8000016d" null null;
 | 
			
		||||
      run = fs "800001c0" {
 | 
			
		||||
        current-system = fs "8000016d" null null;
 | 
			
		||||
        opengl-driver = fs "8000016d" null null;
 | 
			
		||||
        user = fs "800001ed" {
 | 
			
		||||
          "65534" = fs "800001ed" {
 | 
			
		||||
            bus = fs "10001fd" null null;
 | 
			
		||||
            pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
 | 
			
		||||
            wayland-0 = fs "1000038" null null;
 | 
			
		||||
          } null;
 | 
			
		||||
        } null;
 | 
			
		||||
      } null;
 | 
			
		||||
      sys = fs "800001c0" {
 | 
			
		||||
        block = fs "800001ed" {
 | 
			
		||||
          fd0 = fs "80001ff" null null;
 | 
			
		||||
          loop0 = fs "80001ff" null null;
 | 
			
		||||
          loop1 = fs "80001ff" null null;
 | 
			
		||||
          loop2 = fs "80001ff" null null;
 | 
			
		||||
          loop3 = fs "80001ff" null null;
 | 
			
		||||
          loop4 = fs "80001ff" null null;
 | 
			
		||||
          loop5 = fs "80001ff" null null;
 | 
			
		||||
          loop6 = fs "80001ff" null null;
 | 
			
		||||
          loop7 = fs "80001ff" null null;
 | 
			
		||||
          sr0 = fs "80001ff" null null;
 | 
			
		||||
          vda = fs "80001ff" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        bus = fs "800001ed" null null;
 | 
			
		||||
        class = fs "800001ed" null null;
 | 
			
		||||
        dev = fs "800001ed" {
 | 
			
		||||
          block = fs "800001ed" null null;
 | 
			
		||||
          char = fs "800001ed" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        devices = fs "800001ed" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      tmp = fs "800001f8" { } null;
 | 
			
		||||
      usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
 | 
			
		||||
      var = fs "800001c0" {
 | 
			
		||||
        lib = fs "800001c0" {
 | 
			
		||||
          fortify = fs "800001c0" {
 | 
			
		||||
            u0 = fs "800001c0" {
 | 
			
		||||
              a2 = fs "800001c0" {
 | 
			
		||||
                ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
 | 
			
		||||
                ".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
 | 
			
		||||
                ".local" = fs "800001ed" {
 | 
			
		||||
                  state = fs "800001ed" {
 | 
			
		||||
                    home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
 | 
			
		||||
                    nix = fs "800001ed" {
 | 
			
		||||
                      profiles = fs "800001ed" {
 | 
			
		||||
                        home-manager = fs "80001ff" null null;
 | 
			
		||||
                        home-manager-1-link = fs "80001ff" null null;
 | 
			
		||||
                        profile = fs "80001ff" null null;
 | 
			
		||||
                        profile-1-link = fs "80001ff" null null;
 | 
			
		||||
                      } null;
 | 
			
		||||
                    } null;
 | 
			
		||||
                  } null;
 | 
			
		||||
                } null;
 | 
			
		||||
                ".nix-defexpr" = fs "800001ed" {
 | 
			
		||||
                  channels = fs "80001ff" null null;
 | 
			
		||||
                  channels_root = fs "80001ff" null null;
 | 
			
		||||
                } null;
 | 
			
		||||
                ".nix-profile" = fs "80001ff" null null;
 | 
			
		||||
              } null;
 | 
			
		||||
            } null;
 | 
			
		||||
          } null;
 | 
			
		||||
        } null;
 | 
			
		||||
        run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
 | 
			
		||||
      } null;
 | 
			
		||||
    } null;
 | 
			
		||||
 | 
			
		||||
    mount = [
 | 
			
		||||
      (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
 | 
			
		||||
      (ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
 | 
			
		||||
      (ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
 | 
			
		||||
      (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
 | 
			
		||||
      (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent "/tmp/fortify.1000/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/var/lib/fortify/u0/a2" "/var/lib/fortify/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
 | 
			
		||||
      (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
 | 
			
		||||
      (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000002,gid=1000002")
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    seccomp = true;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -1,14 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  writeShellScript,
 | 
			
		||||
  writeText,
 | 
			
		||||
  callPackage,
 | 
			
		||||
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
name: want:
 | 
			
		||||
writeShellScript "fortify-${name}-check-sandbox-script" ''
 | 
			
		||||
writeShellScript "check-sandbox" ''
 | 
			
		||||
  set -e
 | 
			
		||||
  ${callPackage ./assert.nix { inherit version; }}/bin/test \
 | 
			
		||||
    ${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
 | 
			
		||||
  ${callPackage ./mount.nix { inherit version; }}/bin/test
 | 
			
		||||
  ${callPackage ./fs.nix { inherit version; }}/bin/test
 | 
			
		||||
  ${callPackage ./seccomp.nix { inherit version; }}/bin/test
 | 
			
		||||
 | 
			
		||||
  touch /tmp/sandbox-ok
 | 
			
		||||
''
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ func printDir(prefix string, dir []fs.DirEntry) {
 | 
			
		||||
		}
 | 
			
		||||
		names[i] = fmt.Sprintf("%q", name)
 | 
			
		||||
	}
 | 
			
		||||
	printf("[FAIL] d %s: %s", prefix, strings.Join(names, " "))
 | 
			
		||||
	printf("[FAIL] d %q: %s", prefix, strings.Join(names, " "))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *FS) Compare(prefix string, e fs.FS) error {
 | 
			
		||||
@ -71,7 +71,7 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
 | 
			
		||||
			if fi, err := got.Info(); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			} else if fi.Mode() != want.Mode {
 | 
			
		||||
				printf("[FAIL] m %s: %x, want %x",
 | 
			
		||||
				printf("[FAIL] m %q: %x, want %x",
 | 
			
		||||
					name, uint32(fi.Mode()), uint32(want.Mode))
 | 
			
		||||
				return ErrFSBadMode
 | 
			
		||||
			}
 | 
			
		||||
@ -84,8 +84,6 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
 | 
			
		||||
					return err
 | 
			
		||||
				} else if string(v) != *want.Data {
 | 
			
		||||
					printf("[FAIL] f %s", name)
 | 
			
		||||
					printf("got:  %s", v)
 | 
			
		||||
					printf("want: %s", *want.Data)
 | 
			
		||||
					return ErrFSBadData
 | 
			
		||||
				}
 | 
			
		||||
				printf("[ OK ] f %s", name)
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,29 @@
 | 
			
		||||
{
 | 
			
		||||
  fs,
 | 
			
		||||
  ent,
 | 
			
		||||
  ignore,
 | 
			
		||||
}:
 | 
			
		||||
{
 | 
			
		||||
  name = "preset";
 | 
			
		||||
  tty = false;
 | 
			
		||||
  mapRealUid = false;
 | 
			
		||||
  lib,
 | 
			
		||||
  writeText,
 | 
			
		||||
  buildGoModule,
 | 
			
		||||
 | 
			
		||||
  want = {
 | 
			
		||||
    fs = fs "dead" {
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
let
 | 
			
		||||
  wantFS =
 | 
			
		||||
    let
 | 
			
		||||
      fs = mode: dir: data: {
 | 
			
		||||
        mode = lib.fromHexString mode;
 | 
			
		||||
        inherit
 | 
			
		||||
          dir
 | 
			
		||||
          data
 | 
			
		||||
          ;
 | 
			
		||||
      };
 | 
			
		||||
    in
 | 
			
		||||
    fs "dead" {
 | 
			
		||||
      ".fortify" = fs "800001ed" {
 | 
			
		||||
        etc = fs "800001ed" null null;
 | 
			
		||||
        sbin = fs "800001c0" {
 | 
			
		||||
          fortify = fs "16d" null null;
 | 
			
		||||
          init0 = fs "80001ff" null null;
 | 
			
		||||
        } null;
 | 
			
		||||
        host-mounts = fs "124" null null;
 | 
			
		||||
      } null;
 | 
			
		||||
      bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
 | 
			
		||||
      dev = fs "800001ed" {
 | 
			
		||||
@ -179,43 +191,24 @@
 | 
			
		||||
      } null;
 | 
			
		||||
    } null;
 | 
			
		||||
 | 
			
		||||
    mount = [
 | 
			
		||||
      (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
 | 
			
		||||
      (ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
 | 
			
		||||
      (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
 | 
			
		||||
      (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
 | 
			
		||||
      (ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
 | 
			
		||||
      (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
 | 
			
		||||
      (ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent "/tmp/fortify.1000/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/var/lib/fortify/u0/a1" "/var/lib/fortify/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
 | 
			
		||||
      (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
 | 
			
		||||
      (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
 | 
			
		||||
      (ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000001,gid=1000001")
 | 
			
		||||
    ];
 | 
			
		||||
  mainFile = writeText "main.go" ''
 | 
			
		||||
    package main
 | 
			
		||||
 | 
			
		||||
    seccomp = true;
 | 
			
		||||
  };
 | 
			
		||||
    import "os"
 | 
			
		||||
    import "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
 | 
			
		||||
    func main() { sandbox.MustAssertFS(os.DirFS("/"), "${writeText "want-fs.json" (builtins.toJSON wantFS)}") }
 | 
			
		||||
  '';
 | 
			
		||||
in
 | 
			
		||||
buildGoModule {
 | 
			
		||||
  pname = "check-fs";
 | 
			
		||||
  inherit version;
 | 
			
		||||
 | 
			
		||||
  src = ../.;
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
  preBuild = ''
 | 
			
		||||
    go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
 | 
			
		||||
    cp ${mainFile} main.go
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
@ -31,16 +31,16 @@ func TestCompare(t *testing.T) {
 | 
			
		||||
			"[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
 | 
			
		||||
		{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
 | 
			
		||||
			&sandbox.FS{Dir: make(map[string]*sandbox.FS)},
 | 
			
		||||
			"[FAIL] d .: \".fortify/\"\x00", sandbox.ErrFSBadLength},
 | 
			
		||||
			"[FAIL] d \".\": \".fortify/\"\x00", sandbox.ErrFSBadLength},
 | 
			
		||||
		{"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
 | 
			
		||||
			&sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}},
 | 
			
		||||
			"[FAIL] m .fortify: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
 | 
			
		||||
			"[FAIL] m \".fortify\": 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
 | 
			
		||||
		{"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
 | 
			
		||||
			&sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
 | 
			
		||||
			"[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt},
 | 
			
		||||
			"[FAIL] d \".\": \"test\"\x00", sandbox.ErrFSInvalidEnt},
 | 
			
		||||
		{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
 | 
			
		||||
			&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
 | 
			
		||||
			"[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
 | 
			
		||||
			"[FAIL] d \".\": \"test\"\x00", fs.ErrNotExist},
 | 
			
		||||
		{"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
 | 
			
		||||
			"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
 | 
			
		||||
			"etc/group":  {Data: []byte(fsGroupSample), Mode: 0644},
 | 
			
		||||
@ -54,7 +54,7 @@ func TestCompare(t *testing.T) {
 | 
			
		||||
		}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
 | 
			
		||||
			"passwd": {Mode: 0x1a4, Data: &fsGroupSample},
 | 
			
		||||
			"group":  {Mode: 0x1a4, Data: &fsGroupSample},
 | 
			
		||||
		}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got:  u0_a20:x:65534:65534:Fortify:/var/lib/persist/module/fortify/u0/a20:/run/current-system/sw/bin/zsh\x00want: fortify:x:65534:\x00", sandbox.ErrFSBadData},
 | 
			
		||||
		}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00", sandbox.ErrFSBadData},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
@ -75,4 +75,10 @@ func TestCompare(t *testing.T) {
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("assert", func(t *testing.T) {
 | 
			
		||||
		oldFatal := sandbox.SwapFatal(t.Fatalf)
 | 
			
		||||
		t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
 | 
			
		||||
		sandbox.MustAssertFS(make(fstest.MapFS), sandbox.MustWantFile(t, &sandbox.FS{Mode: 0xDEADBEEF}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,157 +1,146 @@
 | 
			
		||||
package sandbox
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
#cgo linux pkg-config: --static mount
 | 
			
		||||
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <libmount.h>
 | 
			
		||||
#include <mntent.h>
 | 
			
		||||
 | 
			
		||||
const char *F_MOUNTINFO_PATH = "/proc/self/mountinfo";
 | 
			
		||||
const char *F_PROC_MOUNTS = "";
 | 
			
		||||
const char *F_SET_TYPE = "r";
 | 
			
		||||
*/
 | 
			
		||||
import "C"
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrMountinfoParse = errors.New("invalid mountinfo records")
 | 
			
		||||
	ErrMountinfoIter  = errors.New("cannot allocate iterator")
 | 
			
		||||
	ErrMountinfoFault = errors.New("cannot iterate on filesystems")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	Mountinfo struct {
 | 
			
		||||
		mu  sync.RWMutex
 | 
			
		||||
		p   string
 | 
			
		||||
		err error
 | 
			
		||||
 | 
			
		||||
		tb  *C.struct_libmnt_table
 | 
			
		||||
		itr *C.struct_libmnt_iter
 | 
			
		||||
 | 
			
		||||
		fs *C.struct_libmnt_fs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry.
 | 
			
		||||
	MountinfoEntry struct {
 | 
			
		||||
		// mount ID: a unique ID for the mount (may be reused after umount(2)).
 | 
			
		||||
		ID int `json:"id"`
 | 
			
		||||
		// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
 | 
			
		||||
		Parent int `json:"parent"`
 | 
			
		||||
		// root: the pathname of the directory in the filesystem which forms the root of this mount.
 | 
			
		||||
		Root string `json:"root"`
 | 
			
		||||
		// mount point: the pathname of the mount point relative to the process's root directory.
 | 
			
		||||
		Target string `json:"target"`
 | 
			
		||||
		// mount options: per-mount options (see mount(2)).
 | 
			
		||||
		VfsOptstr string `json:"vfs_optstr"`
 | 
			
		||||
		// filesystem type: the filesystem type in the form "type[.subtype]".
 | 
			
		||||
		FsType string `json:"fstype"`
 | 
			
		||||
		// mount source: filesystem-specific information or "none".
 | 
			
		||||
		Source string `json:"source"`
 | 
			
		||||
		// super options: per-superblock options (see mount(2)).
 | 
			
		||||
		FsOptstr string `json:"fs_optstr"`
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (m *Mountinfo) copy(v *MountinfoEntry) {
 | 
			
		||||
	if m.fs == nil {
 | 
			
		||||
		panic("invalid entry")
 | 
			
		||||
	}
 | 
			
		||||
	v.ID = int(C.mnt_fs_get_id(m.fs))
 | 
			
		||||
	v.Parent = int(C.mnt_fs_get_parent_id(m.fs))
 | 
			
		||||
	v.Root = C.GoString(C.mnt_fs_get_root(m.fs))
 | 
			
		||||
	v.Target = C.GoString(C.mnt_fs_get_target(m.fs))
 | 
			
		||||
	v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs))
 | 
			
		||||
	v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs))
 | 
			
		||||
	v.Source = C.GoString(C.mnt_fs_get_source(m.fs))
 | 
			
		||||
	v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs))
 | 
			
		||||
type Mntent struct {
 | 
			
		||||
	/* name of mounted filesystem */
 | 
			
		||||
	FSName string `json:"fsname"`
 | 
			
		||||
	/* filesystem path prefix */
 | 
			
		||||
	Dir string `json:"dir"`
 | 
			
		||||
	/* mount type (see mntent.h) */
 | 
			
		||||
	Type string `json:"type"`
 | 
			
		||||
	/* mount options (see mntent.h) */
 | 
			
		||||
	Opts string `json:"opts"`
 | 
			
		||||
	/* dump frequency in days */
 | 
			
		||||
	Freq int `json:"freq"`
 | 
			
		||||
	/* pass number on parallel fsck */
 | 
			
		||||
	Passno int `json:"passno"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
 | 
			
		||||
func (e *Mntent) String() string {
 | 
			
		||||
	return fmt.Sprintf("%s %s %s %s %d %d",
 | 
			
		||||
		e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
 | 
			
		||||
func (e *Mntent) Is(want *Mntent) bool {
 | 
			
		||||
	if want == nil {
 | 
			
		||||
		return e == nil
 | 
			
		||||
	}
 | 
			
		||||
	return (e.FSName == want.FSName || want.FSName == "\x00") &&
 | 
			
		||||
		(e.Dir == want.Dir || want.Dir == "\x00") &&
 | 
			
		||||
		(e.Type == want.Type || want.Type == "\x00") &&
 | 
			
		||||
		(e.Opts == want.Opts || want.Opts == "\x00") &&
 | 
			
		||||
		(e.Freq == want.Freq || want.Freq == -1) &&
 | 
			
		||||
		(e.Passno == want.Passno || want.Passno == -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mountinfo) Parse() error {
 | 
			
		||||
func IterMounts(name string, f func(e *Mntent)) error {
 | 
			
		||||
	m := new(mounts)
 | 
			
		||||
	m.p = name
 | 
			
		||||
	if err := m.open(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for m.scan() {
 | 
			
		||||
		e := new(Mntent)
 | 
			
		||||
		m.copy(e)
 | 
			
		||||
		f(e)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.close()
 | 
			
		||||
	return m.Err()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type mounts struct {
 | 
			
		||||
	p  string
 | 
			
		||||
	f  *C.FILE
 | 
			
		||||
	mu sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	ent *C.struct_mntent
 | 
			
		||||
	err error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mounts) open() error {
 | 
			
		||||
	m.mu.Lock()
 | 
			
		||||
	defer m.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if m.tb != nil {
 | 
			
		||||
	if m.f != nil {
 | 
			
		||||
		panic("open called twice")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.p == "" {
 | 
			
		||||
		m.tb = C.mnt_new_table_from_file(C.F_MOUNTINFO_PATH)
 | 
			
		||||
	} else {
 | 
			
		||||
		name := C.CString(m.p)
 | 
			
		||||
		m.tb = C.mnt_new_table_from_file(name)
 | 
			
		||||
		C.free(unsafe.Pointer(name))
 | 
			
		||||
	}
 | 
			
		||||
	if m.tb == nil {
 | 
			
		||||
		return ErrMountinfoParse
 | 
			
		||||
	}
 | 
			
		||||
	m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
 | 
			
		||||
	if m.itr == nil {
 | 
			
		||||
		C.mnt_unref_table(m.tb)
 | 
			
		||||
		return ErrMountinfoIter
 | 
			
		||||
		m.p = "/proc/mounts"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runtime.SetFinalizer(m, (*Mountinfo).Unref)
 | 
			
		||||
	return nil
 | 
			
		||||
	name := C.CString(m.p)
 | 
			
		||||
	f, err := C.setmntent(name, C.F_SET_TYPE)
 | 
			
		||||
	C.free(unsafe.Pointer(name))
 | 
			
		||||
 | 
			
		||||
	if f == nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	m.f = f
 | 
			
		||||
	runtime.SetFinalizer(m, (*mounts).close)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mountinfo) Unref() {
 | 
			
		||||
func (m *mounts) close() {
 | 
			
		||||
	m.mu.Lock()
 | 
			
		||||
	defer m.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if m.tb == nil {
 | 
			
		||||
		panic("unref called before parse")
 | 
			
		||||
	if m.f == nil {
 | 
			
		||||
		panic("close called before open")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	C.mnt_unref_table(m.tb)
 | 
			
		||||
	C.mnt_free_iter(m.itr)
 | 
			
		||||
	C.endmntent(m.f)
 | 
			
		||||
	runtime.SetFinalizer(m, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] {
 | 
			
		||||
	return func(yield func(*MountinfoEntry) bool) {
 | 
			
		||||
		m.mu.Lock()
 | 
			
		||||
		defer m.mu.Unlock()
 | 
			
		||||
func (m *mounts) scan() bool {
 | 
			
		||||
	m.mu.Lock()
 | 
			
		||||
	defer m.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
		C.mnt_reset_iter(m.itr, -1)
 | 
			
		||||
 | 
			
		||||
		var rc C.int
 | 
			
		||||
		ent := new(MountinfoEntry)
 | 
			
		||||
		for rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs); rc == 0; rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs) {
 | 
			
		||||
			m.copy(ent)
 | 
			
		||||
			if !yield(ent) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if rc < 0 {
 | 
			
		||||
			m.err = ErrMountinfoFault
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	if m.f == nil {
 | 
			
		||||
		panic("invalid file")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.ent, m.err = C.getmntent(m.f)
 | 
			
		||||
	return m.ent != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool {
 | 
			
		||||
	return (e.ID == want.ID || want.ID == -1) &&
 | 
			
		||||
		(e.Parent == want.Parent || want.Parent == -1) &&
 | 
			
		||||
		(e.Root == want.Root || want.Root == ignore) &&
 | 
			
		||||
		(e.Target == want.Target || want.Target == ignore) &&
 | 
			
		||||
		(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
 | 
			
		||||
		(e.FsType == want.FsType || want.FsType == ignore) &&
 | 
			
		||||
		(e.Source == want.Source || want.Source == ignore) &&
 | 
			
		||||
		(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
 | 
			
		||||
func (m *mounts) Err() error {
 | 
			
		||||
	m.mu.RLock()
 | 
			
		||||
	defer m.mu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return m.err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *MountinfoEntry) String() string {
 | 
			
		||||
	return fmt.Sprintf("%d %d %s %s %s %s %s %s",
 | 
			
		||||
		e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr)
 | 
			
		||||
func (m *mounts) copy(v *Mntent) {
 | 
			
		||||
	m.mu.RLock()
 | 
			
		||||
	defer m.mu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	if m.ent == nil {
 | 
			
		||||
		panic("invalid entry")
 | 
			
		||||
	}
 | 
			
		||||
	v.FSName = C.GoString(m.ent.mnt_fsname)
 | 
			
		||||
	v.Dir = C.GoString(m.ent.mnt_dir)
 | 
			
		||||
	v.Type = C.GoString(m.ent.mnt_type)
 | 
			
		||||
	v.Opts = C.GoString(m.ent.mnt_opts)
 | 
			
		||||
	v.Freq = int(m.ent.mnt_freq)
 | 
			
		||||
	v.Passno = int(m.ent.mnt_passno)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										79
									
								
								test/sandbox/mount.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								test/sandbox/mount.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
			
		||||
{
 | 
			
		||||
  writeText,
 | 
			
		||||
  buildGoModule,
 | 
			
		||||
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
let
 | 
			
		||||
  wantMounts =
 | 
			
		||||
    let
 | 
			
		||||
      ent = fsname: dir: type: opts: freq: passno: {
 | 
			
		||||
        inherit
 | 
			
		||||
          fsname
 | 
			
		||||
          dir
 | 
			
		||||
          type
 | 
			
		||||
          opts
 | 
			
		||||
          freq
 | 
			
		||||
          passno
 | 
			
		||||
          ;
 | 
			
		||||
      };
 | 
			
		||||
    in
 | 
			
		||||
    [
 | 
			
		||||
      (ent "tmpfs" "/" "tmpfs" "rw,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "proc" "/proc" "proc" "rw,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/.fortify" "tmpfs" "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/dev" "tmpfs" "rw,nosuid,nodev,relatime,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/null" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/zero" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/full" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/random" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/urandom" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/tty" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "devpts" "/dev/pts" "devpts" "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666" 0 0)
 | 
			
		||||
      (ent "mqueue" "/dev/mqueue" "mqueue" "rw,relatime" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/usr/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "overlay" "/nix/store" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
			
		||||
      (ent "overlay" "/run/current-system" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
			
		||||
      (ent "sysfs" "/sys/block" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "sysfs" "/sys/bus" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "sysfs" "/sys/class" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "sysfs" "/sys/dev" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "sysfs" "/sys/devices" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "overlay" "/run/opengl-driver" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
			
		||||
      (ent "devtmpfs" "/dev/dri" "devtmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "proc" "/.fortify/host-mounts" "proc" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/.fortify/etc" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/run/user" "tmpfs" "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/run/user/65534" "tmpfs" "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/tmp" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/var/lib/fortify/u0/a1" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "host_passthrough" 0 0)
 | 
			
		||||
      (ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
			
		||||
      (ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
			
		||||
      (ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
  mainFile = writeText "main.go" ''
 | 
			
		||||
    package main
 | 
			
		||||
 | 
			
		||||
    import "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
 | 
			
		||||
    func main() { sandbox.MustAssertMounts("", "/.fortify/host-mounts", "${writeText "want-mounts.json" (builtins.toJSON wantMounts)}") }
 | 
			
		||||
  '';
 | 
			
		||||
in
 | 
			
		||||
buildGoModule {
 | 
			
		||||
  pname = "check-mounts";
 | 
			
		||||
  inherit version;
 | 
			
		||||
 | 
			
		||||
  src = ../.;
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
  preBuild = ''
 | 
			
		||||
    go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
 | 
			
		||||
    cp ${mainFile} main.go
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
@ -8,79 +8,80 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMountinfo(t *testing.T) {
 | 
			
		||||
func TestMounts(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
 | 
			
		||||
		sample string
 | 
			
		||||
		want   []*sandbox.MountinfoEntry
 | 
			
		||||
		want   []sandbox.Mntent
 | 
			
		||||
	}{
 | 
			
		||||
		{"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw
 | 
			
		||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
 | 
			
		||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
 | 
			
		||||
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
 | 
			
		||||
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
 | 
			
		||||
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
 | 
			
		||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
 | 
			
		||||
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
 | 
			
		||||
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
 | 
			
		||||
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
 | 
			
		||||
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
 | 
			
		||||
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
 | 
			
		||||
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
 | 
			
		||||
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
 | 
			
		||||
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
 | 
			
		||||
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
 | 
			
		||||
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
 | 
			
		||||
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
			
		||||
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
 | 
			
		||||
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
 | 
			
		||||
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
 | 
			
		||||
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
 | 
			
		||||
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
 | 
			
		||||
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
 | 
			
		||||
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
 | 
			
		||||
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
 | 
			
		||||
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
 | 
			
		||||
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
 | 
			
		||||
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{
 | 
			
		||||
			e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
 | 
			
		||||
			e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
 | 
			
		||||
			e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
 | 
			
		||||
			e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
 | 
			
		||||
			e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
 | 
			
		||||
			e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
 | 
			
		||||
			e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
 | 
			
		||||
			e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
 | 
			
		||||
			e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
 | 
			
		||||
			e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
 | 
			
		||||
			e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
 | 
			
		||||
			e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
 | 
			
		||||
			e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
 | 
			
		||||
			e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
 | 
			
		||||
			e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
 | 
			
		||||
			e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
 | 
			
		||||
			e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
 | 
			
		||||
			e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
			
		||||
			e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
			
		||||
			e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
			
		||||
			e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
			
		||||
			e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
			
		||||
			e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
 | 
			
		||||
			e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
 | 
			
		||||
			e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
 | 
			
		||||
			e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
 | 
			
		||||
			e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
 | 
			
		||||
			e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
 | 
			
		||||
			e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
 | 
			
		||||
			e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
 | 
			
		||||
			e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
 | 
			
		||||
			e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"),
 | 
			
		||||
			e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
 | 
			
		||||
		{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
			
		||||
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
 | 
			
		||||
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
 | 
			
		||||
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
 | 
			
		||||
mqueue /dev/mqueue mqueue rw,relatime 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
 | 
			
		||||
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
 | 
			
		||||
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
			
		||||
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
 | 
			
		||||
/dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0
 | 
			
		||||
overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on 0 0
 | 
			
		||||
`, []sandbox.Mntent{
 | 
			
		||||
			{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
 | 
			
		||||
			{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
 | 
			
		||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
			
		||||
			{"overlay", "/.fortify/sbin/fortify", "overlay", "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on", 0, 0},
 | 
			
		||||
		}},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -91,33 +92,27 @@ func TestMountinfo(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			m := sandbox.NewMountinfo(name)
 | 
			
		||||
			if err := m.Parse(); err != nil {
 | 
			
		||||
				t.Fatalf("Parse: error = %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i := 0
 | 
			
		||||
			for ent := range m.Entries() {
 | 
			
		||||
			if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
 | 
			
		||||
				if i == len(tc.want) {
 | 
			
		||||
					t.Errorf("Entries: got more than %d entries", i)
 | 
			
		||||
					t.Errorf("IterMounts: got more than %d entries", i)
 | 
			
		||||
					t.FailNow()
 | 
			
		||||
				}
 | 
			
		||||
				if !ent.EqualWithIgnore(tc.want[i], "\x00") {
 | 
			
		||||
					t.Errorf("Entries: entry %d\n got: %#v\nwant: %#v", i,
 | 
			
		||||
						ent, &tc.want[i])
 | 
			
		||||
				if *e != tc.want[i] {
 | 
			
		||||
					t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
 | 
			
		||||
						e, &tc.want[i])
 | 
			
		||||
					t.FailNow()
 | 
			
		||||
				} else {
 | 
			
		||||
					t.Logf("%s", ent)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				i++
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				t.Fatalf("IterMounts: error = %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
			if err := m.Err(); err != nil {
 | 
			
		||||
				t.Fatalf("Mountinfo: error = %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			m.Unref()
 | 
			
		||||
		t.Run(tc.name+" assert", func(t *testing.T) {
 | 
			
		||||
			oldFatal := sandbox.SwapFatal(t.Fatalf)
 | 
			
		||||
			t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
 | 
			
		||||
			sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want))
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if err := os.Remove(name); err != nil {
 | 
			
		||||
@ -125,18 +120,3 @@ func TestMountinfo(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func e(
 | 
			
		||||
	id, parent int, root, target, vfsOptstr string, fsType, source, fsOptstr string,
 | 
			
		||||
) *sandbox.MountinfoEntry {
 | 
			
		||||
	return &sandbox.MountinfoEntry{
 | 
			
		||||
		ID:        id,
 | 
			
		||||
		Parent:    parent,
 | 
			
		||||
		Root:      root,
 | 
			
		||||
		Target:    target,
 | 
			
		||||
		VfsOptstr: vfsOptstr,
 | 
			
		||||
		FsType:    fsType,
 | 
			
		||||
		Source:    source,
 | 
			
		||||
		FsOptstr:  fsOptstr,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								test/sandbox/seccomp.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								test/sandbox/seccomp.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
{
 | 
			
		||||
  writeText,
 | 
			
		||||
  buildGoModule,
 | 
			
		||||
 | 
			
		||||
  version,
 | 
			
		||||
}:
 | 
			
		||||
let
 | 
			
		||||
  mainFile = writeText "main.go" ''
 | 
			
		||||
    package main
 | 
			
		||||
 | 
			
		||||
    import "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
			
		||||
 | 
			
		||||
    func main() { sandbox.MustAssertSeccomp() }
 | 
			
		||||
  '';
 | 
			
		||||
in
 | 
			
		||||
buildGoModule {
 | 
			
		||||
  pname = "check-seccomp";
 | 
			
		||||
  inherit version;
 | 
			
		||||
 | 
			
		||||
  src = ../.;
 | 
			
		||||
  vendorHash = null;
 | 
			
		||||
 | 
			
		||||
  preBuild = ''
 | 
			
		||||
    go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
 | 
			
		||||
    cp ${mainFile} main.go
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								test/test.py
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								test/test.py
									
									
									
									
									
								
							@ -62,12 +62,9 @@ def check_state(name, enablements):
 | 
			
		||||
 | 
			
		||||
    config = instance['config']
 | 
			
		||||
 | 
			
		||||
    command = f"{name}-start"
 | 
			
		||||
    if not (config['path'].startswith("/nix/store/")) or not (config['path'].endswith(command)):
 | 
			
		||||
        raise Exception(f"unexpected path {config['path']}")
 | 
			
		||||
 | 
			
		||||
    if len(config['args']) != 1 or config['args'][0] != command:
 | 
			
		||||
        raise Exception(f"unexpected args {config['args']}")
 | 
			
		||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not (
 | 
			
		||||
            config['command'][0].endswith(f"{name}-start")):
 | 
			
		||||
        raise Exception(f"unexpected command {instance['config']['command']}")
 | 
			
		||||
 | 
			
		||||
    if config['confinement']['enablements'] != enablements:
 | 
			
		||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
			
		||||
@ -105,26 +102,9 @@ if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
			
		||||
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
			
		||||
    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
			
		||||
 | 
			
		||||
# Check sandbox outcome:
 | 
			
		||||
check_offset = 0
 | 
			
		||||
def check_sandbox(name):
 | 
			
		||||
    global check_offset
 | 
			
		||||
    check_offset += 1
 | 
			
		||||
    swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
 | 
			
		||||
    machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
check_sandbox("preset")
 | 
			
		||||
check_sandbox("tty")
 | 
			
		||||
check_sandbox("mapuid")
 | 
			
		||||
 | 
			
		||||
def aid(offset):
 | 
			
		||||
    return 1+check_offset+offset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def tmpdir_path(offset, name):
 | 
			
		||||
    return f"/tmp/fortify.1000/tmpdir/{aid(offset)}/{name}"
 | 
			
		||||
 | 
			
		||||
# Check sandbox state:
 | 
			
		||||
swaymsg("exec check-sandbox")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15)
 | 
			
		||||
 | 
			
		||||
# Start fortify permissive defaults outside Wayland session:
 | 
			
		||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
			
		||||
@ -166,23 +146,23 @@ machine.succeed("pkill -9 mako")
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with Wayland enablement:
 | 
			
		||||
swaymsg("exec ne-foot")
 | 
			
		||||
wait_for_window(f"u0_a{aid(0)}@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
 | 
			
		||||
machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=10)
 | 
			
		||||
wait_for_window("u0_a2@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10)
 | 
			
		||||
collect_state_ui("foot_wayland")
 | 
			
		||||
check_state("ne-foot", 1)
 | 
			
		||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
			
		||||
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}"))
 | 
			
		||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
machine.wait_until_fails("pgrep foot", timeout=5)
 | 
			
		||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
			
		||||
machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
 | 
			
		||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002", timeout=5)
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with Wayland enablement from a terminal:
 | 
			
		||||
swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
			
		||||
wait_for_window(f"u0_a{aid(0)}@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
 | 
			
		||||
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=10)
 | 
			
		||||
wait_for_window("u0_a2@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10)
 | 
			
		||||
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
 | 
			
		||||
collect_state_ui("foot_wayland_term")
 | 
			
		||||
check_state("ne-foot", 1)
 | 
			
		||||
@ -193,9 +173,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
 | 
			
		||||
 | 
			
		||||
# Test PulseAudio (fortify does not support PipeWire yet):
 | 
			
		||||
swaymsg("exec pa-foot")
 | 
			
		||||
wait_for_window(f"u0_a{aid(1)}@machine")
 | 
			
		||||
machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n")
 | 
			
		||||
machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15)
 | 
			
		||||
wait_for_window("u0_a3@machine")
 | 
			
		||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10)
 | 
			
		||||
collect_state_ui("pulse_wayland")
 | 
			
		||||
check_state("pa-foot", 9)
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
@ -203,9 +183,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
 | 
			
		||||
 | 
			
		||||
# Test XWayland (foot does not support X):
 | 
			
		||||
swaymsg("exec x11-alacritty")
 | 
			
		||||
wait_for_window(f"u0_a{aid(2)}@machine")
 | 
			
		||||
machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n")
 | 
			
		||||
machine.wait_for_file(tmpdir_path(2, "x11-ok"), timeout=10)
 | 
			
		||||
wait_for_window("u0_a4@machine")
 | 
			
		||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10)
 | 
			
		||||
collect_state_ui("alacritty_x11")
 | 
			
		||||
check_state("x11-alacritty", 2)
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
@ -213,17 +193,17 @@ machine.wait_until_fails("pgrep alacritty", timeout=5)
 | 
			
		||||
 | 
			
		||||
# Start app (foot) with direct Wayland access:
 | 
			
		||||
swaymsg("exec da-foot")
 | 
			
		||||
wait_for_window(f"u0_a{aid(3)}@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n")
 | 
			
		||||
wait_for_window("u0_a5@machine")
 | 
			
		||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
			
		||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
 | 
			
		||||
collect_state_ui("foot_direct")
 | 
			
		||||
machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=10)
 | 
			
		||||
check_state("da-foot", 1)
 | 
			
		||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
			
		||||
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}"))
 | 
			
		||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
 | 
			
		||||
machine.send_chars("exit\n")
 | 
			
		||||
machine.wait_until_fails("pgrep foot", timeout=5)
 | 
			
		||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
			
		||||
machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}", timeout=5)
 | 
			
		||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005", timeout=5)
 | 
			
		||||
 | 
			
		||||
# Test syscall filter:
 | 
			
		||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
			
		||||
 | 
			
		||||
@ -94,7 +94,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
 | 
			
		||||
 | 
			
		||||
			// keep socket alive until done is requested
 | 
			
		||||
			<-done
 | 
			
		||||
			runtime.KeepAlive(syncPipe[1])
 | 
			
		||||
			runtime.KeepAlive(syncPipe[1].Fd())
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			setupDone <- err
 | 
			
		||||
		}
 | 
			
		||||
@ -107,7 +107,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
 | 
			
		||||
	return syncPipe[1], <-setupDone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error {
 | 
			
		||||
func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
 | 
			
		||||
	// ensure p is available
 | 
			
		||||
	if f, err := os.Create(p); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -117,5 +117,5 @@ func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bindWaylandFd(p, fd, appID, instanceID, syncFd)
 | 
			
		||||
	return bindWaylandFd(p, fd, appID, instanceID, syncFD)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								wl/wl.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								wl/wl.go
									
									
									
									
									
								
							@ -25,11 +25,11 @@ var resErr = [...]error{
 | 
			
		||||
	2: errors.New("wp_security_context_v1 not available"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error {
 | 
			
		||||
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error {
 | 
			
		||||
	if hasNull(appID) || hasNull(instanceID) {
 | 
			
		||||
		return ErrContainsNull
 | 
			
		||||
	}
 | 
			
		||||
	res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd))
 | 
			
		||||
	res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
 | 
			
		||||
	return resErr[int32(res)]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user