Compare commits
	
		
			46 Commits
		
	
	
		
			24618ab9a1
			...
			5c4058d5ac
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5c4058d5ac | |||
| e732dca762 | |||
| a9adcd914b | |||
| 3dd4ff29c8 | |||
| 61d86c5e10 | |||
| d097eaa28f | |||
| ad3576c164 | |||
| b989a4601a | |||
| a11237b158 | |||
| 40f00d570e | |||
| 0eb1bc6301 | |||
| 1eb837eab8 | |||
| 0a4e633db2 | |||
| e8809125d4 | |||
| 806ce18c0a | |||
| b71d2bf534 | |||
| 46059b1840 | |||
| d2c329bcea | |||
| 2d379b5a38 | |||
| 75e0c5d406 | |||
| 770b37ae16 | |||
| c638193268 | |||
| 8c3a817881 | |||
| e2fce321c1 | |||
| 241702ae3a | |||
| d21d9c5b1d | |||
| a70daf2250 | |||
| 632b18addd | |||
| a57a7a6a16 | |||
| 5098b12e4a | |||
| 9ddf5794dd | |||
| b74a08dda9 | |||
| 1b9408864f | |||
| cc89dbdf63 | |||
| 228f3301f2 | |||
| 07181138e5 | |||
| 816b372f14 | |||
| d7eddd54a2 | |||
| 7c063833e0 | |||
| af3619d440 | |||
| 528674cb6e | |||
| 70c9757e26 | |||
| c83a7e2efc | |||
| 904208b87f | |||
| 007b52d81f | |||
| 3385538142 | 
@ -4,12 +4,15 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type bundleInfo struct {
 | 
					type appInfo struct {
 | 
				
			||||||
	Name    string `json:"name"`
 | 
						Name    string `json:"name"`
 | 
				
			||||||
	Version string `json:"version"`
 | 
						Version string `json:"version"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,13 +23,15 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Groups []string `json:"groups,omitempty"`
 | 
						Groups []string `json:"groups,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
						Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
 | 
						Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
						Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
						Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
						Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
						MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
@ -38,11 +43,9 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablements `json:"enablements"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through inverted to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Devel bool `json:"devel,omitempty"`
 | 
					 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
					 | 
				
			||||||
	Multiarch bool `json:"multiarch,omitempty"`
 | 
						Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Bluetooth bool `json:"bluetooth,omitempty"`
 | 
						Bluetooth bool `json:"bluetooth,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// allow gpu access within sandbox
 | 
						// allow gpu access within sandbox
 | 
				
			||||||
@ -59,8 +62,64 @@ type bundleInfo struct {
 | 
				
			|||||||
	ActivationPackage string `json:"activation_package"`
 | 
						ActivationPackage string `json:"activation_package"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
 | 
					func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
 | 
				
			||||||
	bundle := new(bundleInfo)
 | 
						config := &fst.Config{
 | 
				
			||||||
 | 
							ID:   app.ID,
 | 
				
			||||||
 | 
							Path: argv[0],
 | 
				
			||||||
 | 
							Args: argv,
 | 
				
			||||||
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
 | 
								AppID:    app.AppID,
 | 
				
			||||||
 | 
								Groups:   app.Groups,
 | 
				
			||||||
 | 
								Username: "fortify",
 | 
				
			||||||
 | 
								Inner:    path.Join("/data/data", app.ID),
 | 
				
			||||||
 | 
								Outer:    pathSet.homeDir,
 | 
				
			||||||
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
 | 
									Hostname:      formatHostname(app.Name),
 | 
				
			||||||
 | 
									Devel:         app.Devel,
 | 
				
			||||||
 | 
									Userns:        app.Userns,
 | 
				
			||||||
 | 
									Net:           app.Net,
 | 
				
			||||||
 | 
									Dev:           app.Dev,
 | 
				
			||||||
 | 
									Tty:           app.Tty || flagDropShell,
 | 
				
			||||||
 | 
									MapRealUID:    app.MapRealUID,
 | 
				
			||||||
 | 
									DirectWayland: app.DirectWayland,
 | 
				
			||||||
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
										{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
				
			||||||
 | 
										{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
				
			||||||
 | 
										{Src: "/etc/resolv.conf"},
 | 
				
			||||||
 | 
										{Src: "/sys/block"},
 | 
				
			||||||
 | 
										{Src: "/sys/bus"},
 | 
				
			||||||
 | 
										{Src: "/sys/class"},
 | 
				
			||||||
 | 
										{Src: "/sys/dev"},
 | 
				
			||||||
 | 
										{Src: "/sys/devices"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Link: [][2]string{
 | 
				
			||||||
 | 
										{app.CurrentSystem, "/run/current-system"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/bin"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/usr/bin"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
				
			||||||
 | 
									AutoEtc: true,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								ExtraPerms: []*fst.ExtraPermConfig{
 | 
				
			||||||
 | 
									{Path: dataHome, Execute: true},
 | 
				
			||||||
 | 
									{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								SystemBus:   app.SystemBus,
 | 
				
			||||||
 | 
								SessionBus:  app.SessionBus,
 | 
				
			||||||
 | 
								Enablements: app.Enablements,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Multiarch {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Bluetooth {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadAppInfo(name string, beforeFail func()) *appInfo {
 | 
				
			||||||
 | 
						bundle := new(appInfo)
 | 
				
			||||||
	if f, err := os.Open(name); err != nil {
 | 
						if f, err := os.Open(name); err != nil {
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		log.Fatalf("cannot open bundle: %v", err)
 | 
							log.Fatalf("cannot open bundle: %v", err)
 | 
				
			||||||
@ -12,9 +12,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
						"git.gensokyo.uk/security/fortify/command"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/init0"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
						"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
@ -39,7 +37,6 @@ func init() {
 | 
				
			|||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
	sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
	init0.TryArgv0()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
@ -65,9 +62,7 @@ func main() {
 | 
				
			|||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
 | 
							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("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
				
			||||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		var (
 | 
							var (
 | 
				
			||||||
@ -124,7 +119,7 @@ func main() {
 | 
				
			|||||||
				Parse bundle and app metadata, do pre-install checks.
 | 
									Parse bundle and app metadata, do pre-install checks.
 | 
				
			||||||
			*/
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
								bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
 | 
				
			||||||
			pathSet := pathSetByApp(bundle.ID)
 | 
								pathSet := pathSetByApp(bundle.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			app := bundle
 | 
								app := bundle
 | 
				
			||||||
@ -140,7 +135,7 @@ func main() {
 | 
				
			|||||||
				log.Printf("metadata path %q is not a file", pathSet.metaPath)
 | 
									log.Printf("metadata path %q is not a file", pathSet.metaPath)
 | 
				
			||||||
				return syscall.EBADMSG
 | 
									return syscall.EBADMSG
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				app = loadBundleInfo(pathSet.metaPath, cleanup)
 | 
									app = loadAppInfo(pathSet.metaPath, cleanup)
 | 
				
			||||||
				if app.ID != bundle.ID {
 | 
									if app.ID != bundle.ID {
 | 
				
			||||||
					cleanup()
 | 
										cleanup()
 | 
				
			||||||
					log.Printf("app %q claims to have identifier %q",
 | 
										log.Printf("app %q claims to have identifier %q",
 | 
				
			||||||
@ -273,7 +268,7 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			id := args[0]
 | 
								id := args[0]
 | 
				
			||||||
			pathSet := pathSetByApp(id)
 | 
								pathSet := pathSetByApp(id)
 | 
				
			||||||
			app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
								app := loadAppInfo(pathSet.metaPath, func() {})
 | 
				
			||||||
			if app.ID != id {
 | 
								if app.ID != id {
 | 
				
			||||||
				log.Printf("app %q claims to have identifier %q", id, app.ID)
 | 
									log.Printf("app %q claims to have identifier %q", id, app.ID)
 | 
				
			||||||
				return syscall.EBADE
 | 
									return syscall.EBADE
 | 
				
			||||||
@ -322,51 +317,7 @@ func main() {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			argv = append(argv, args[1:]...)
 | 
								argv = append(argv, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			config := &fst.Config{
 | 
								config := app.toFst(pathSet, argv, flagDropShell)
 | 
				
			||||||
				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.
 | 
									Expose GPU devices.
 | 
				
			||||||
 | 
				
			|||||||
@ -11,14 +11,14 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
					func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
 | 
				
			||||||
	rs := new(fst.RunState)
 | 
						rs := new(fst.RunState)
 | 
				
			||||||
	a := app.MustNew(std)
 | 
						a := app.MustNew(ctx, std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if sa, err := a.Seal(config); err != nil {
 | 
						if sa, err := a.Seal(config); err != nil {
 | 
				
			||||||
		fmsg.PrintBaseError(err, "cannot seal app:")
 | 
							fmsg.PrintBaseError(err, "cannot seal app:")
 | 
				
			||||||
		rs.ExitCode = 1
 | 
							rs.ExitCode = 1
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// this updates ExitCode
 | 
							// this updates ExitCode
 | 
				
			||||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
							app.PrintRunStateErr(rs, sa.Run(rs))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if rs.ExitCode != 0 {
 | 
						if rs.ExitCode != 0 {
 | 
				
			||||||
 | 
				
			|||||||
@ -62,8 +62,8 @@ def check_state(name, enablements):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    config = instance['config']
 | 
					    config = instance['config']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['command'][0]):
 | 
					    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 command {instance['config']['command']}")
 | 
					        raise Exception(f"unexpected args {instance['config']['args']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if config['confinement']['enablements'] != enablements:
 | 
					    if config['confinement']['enablements'] != enablements:
 | 
				
			||||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
					        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
				
			||||||
 | 
				
			|||||||
@ -6,18 +6,19 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func withNixDaemon(
 | 
					func withNixDaemon(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
						action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
 | 
				
			||||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
						app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
						mustRunAppDropShell(ctx, updateConfig(&fst.Config{
 | 
				
			||||||
		ID: app.ID,
 | 
							ID:   app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
							Path: shellPath,
 | 
				
			||||||
 | 
							Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
				
			||||||
			// start nix-daemon
 | 
								// start nix-daemon
 | 
				
			||||||
			"nix-daemon --store / & " +
 | 
								"nix-daemon --store / & " +
 | 
				
			||||||
			// wait for socket to appear
 | 
								// wait for socket to appear
 | 
				
			||||||
@ -34,11 +35,11 @@ func withNixDaemon(
 | 
				
			|||||||
			Inner:    path.Join("/data/data", app.ID),
 | 
								Inner:    path.Join("/data/data", app.ID),
 | 
				
			||||||
			Outer:    pathSet.homeDir,
 | 
								Outer:    pathSet.homeDir,
 | 
				
			||||||
			Sandbox: &fst.SandboxConfig{
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname: formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
				UserNS:       true, // nix sandbox requires userns
 | 
									Userns:   true, // nix sandbox requires userns
 | 
				
			||||||
				Net:          net,
 | 
									Net:      net,
 | 
				
			||||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
									Seccomp:  seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									Tty:      dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
										{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
@ -61,19 +62,20 @@ func withNixDaemon(
 | 
				
			|||||||
func withCacheDir(
 | 
					func withCacheDir(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	action string, command []string, workDir string,
 | 
						action string, command []string, workDir string,
 | 
				
			||||||
	app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
						app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	mustRunAppDropShell(ctx, &fst.Config{
 | 
						mustRunAppDropShell(ctx, &fst.Config{
 | 
				
			||||||
		ID:      app.ID,
 | 
							ID:   app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
							Path: shellPath,
 | 
				
			||||||
 | 
							Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
				
			||||||
		Confinement: fst.ConfinementConfig{
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
			AppID:    app.AppID,
 | 
								AppID:    app.AppID,
 | 
				
			||||||
			Username: "nixos",
 | 
								Username: "nixos",
 | 
				
			||||||
			Inner:    path.Join("/data/data", app.ID, "cache"),
 | 
								Inner:    path.Join("/data/data", app.ID, "cache"),
 | 
				
			||||||
			Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
								Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
				
			||||||
			Sandbox: &fst.SandboxConfig{
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname: formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
									Seccomp:  seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									Tty:      dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
										{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
				
			||||||
					{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
										{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
 | 
				
			||||||
@ -97,7 +99,7 @@ func withCacheDir(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
 | 
					func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	if dropShell {
 | 
						if dropShell {
 | 
				
			||||||
		config.Command = []string{shellPath, "-l"}
 | 
							config.Args = []string{shellPath, "-l"}
 | 
				
			||||||
		mustRunApp(ctx, config, beforeFail)
 | 
							mustRunApp(ctx, config, beforeFail)
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		internal.Exit(0)
 | 
							internal.Exit(0)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@ -7,11 +7,11 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1739757849,
 | 
					        "lastModified": 1742234739,
 | 
				
			||||||
        "narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=",
 | 
					        "narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
 | 
				
			||||||
        "owner": "nix-community",
 | 
					        "owner": "nix-community",
 | 
				
			||||||
        "repo": "home-manager",
 | 
					        "repo": "home-manager",
 | 
				
			||||||
        "rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe",
 | 
					        "rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
@ -23,11 +23,11 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "nixpkgs": {
 | 
					    "nixpkgs": {
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1741445498,
 | 
					        "lastModified": 1742512142,
 | 
				
			||||||
        "narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
 | 
					        "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
 | 
				
			||||||
        "owner": "NixOS",
 | 
					        "owner": "NixOS",
 | 
				
			||||||
        "repo": "nixpkgs",
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
        "rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
 | 
					        "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										100
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								flake.nix
									
									
									
									
									
								
							@ -27,7 +27,7 @@
 | 
				
			|||||||
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
					      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
				
			||||||
    in
 | 
					    in
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      nixosModules.fortify = import ./nixos.nix;
 | 
					      nixosModules.fortify = import ./nixos.nix self.packages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      buildPackage = forAllSystems (
 | 
					      buildPackage = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
@ -105,9 +105,21 @@
 | 
				
			|||||||
          default = fortify;
 | 
					          default = fortify;
 | 
				
			||||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
				
			||||||
            inherit (pkgs)
 | 
					            inherit (pkgs)
 | 
				
			||||||
 | 
					              # passthru.buildInputs
 | 
				
			||||||
 | 
					              go
 | 
				
			||||||
 | 
					              gcc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # nativeBuildInputs
 | 
				
			||||||
 | 
					              pkg-config
 | 
				
			||||||
 | 
					              wayland-scanner
 | 
				
			||||||
 | 
					              makeBinaryWrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # appPackages
 | 
				
			||||||
 | 
					              glibc
 | 
				
			||||||
              bubblewrap
 | 
					              bubblewrap
 | 
				
			||||||
              xdg-dbus-proxy
 | 
					              xdg-dbus-proxy
 | 
				
			||||||
              glibc
 | 
					
 | 
				
			||||||
 | 
					              # fpkg
 | 
				
			||||||
              zstd
 | 
					              zstd
 | 
				
			||||||
              gnutar
 | 
					              gnutar
 | 
				
			||||||
              coreutils
 | 
					              coreutils
 | 
				
			||||||
@ -115,7 +127,7 @@
 | 
				
			|||||||
          };
 | 
					          };
 | 
				
			||||||
          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
					          fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
 | 
					          dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
 | 
				
			||||||
            # go requires XDG_CACHE_HOME for the build cache
 | 
					            # go requires XDG_CACHE_HOME for the build cache
 | 
				
			||||||
            export XDG_CACHE_HOME="$(mktemp -d)"
 | 
					            export XDG_CACHE_HOME="$(mktemp -d)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -128,93 +140,21 @@
 | 
				
			|||||||
            export FORTIFY_VERSION="v${fortify.version}"
 | 
					            export FORTIFY_VERSION="v${fortify.version}"
 | 
				
			||||||
            ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
					            ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
 | 
				
			||||||
          '';
 | 
					          '';
 | 
				
			||||||
 | 
					 | 
				
			||||||
          fhs = pkgs.buildFHSEnv {
 | 
					 | 
				
			||||||
            pname = "fortify-fhs";
 | 
					 | 
				
			||||||
            inherit (fortify) version;
 | 
					 | 
				
			||||||
            targetPkgs =
 | 
					 | 
				
			||||||
              pkgs:
 | 
					 | 
				
			||||||
              with pkgs;
 | 
					 | 
				
			||||||
              [
 | 
					 | 
				
			||||||
                go
 | 
					 | 
				
			||||||
                gcc
 | 
					 | 
				
			||||||
                pkg-config
 | 
					 | 
				
			||||||
                wayland-scanner
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
              ++ (
 | 
					 | 
				
			||||||
                with pkgs.pkgsStatic;
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                  musl
 | 
					 | 
				
			||||||
                  libffi
 | 
					 | 
				
			||||||
                  libseccomp
 | 
					 | 
				
			||||||
                  acl
 | 
					 | 
				
			||||||
                  wayland
 | 
					 | 
				
			||||||
                  wayland-protocols
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                ++ (with xorg; [
 | 
					 | 
				
			||||||
                  libxcb
 | 
					 | 
				
			||||||
                  libXau
 | 
					 | 
				
			||||||
                  libXdmcp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  xorgproto
 | 
					 | 
				
			||||||
                ])
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
            extraOutputsToInstall = [ "dev" ];
 | 
					 | 
				
			||||||
            profile = ''
 | 
					 | 
				
			||||||
              export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
 | 
					 | 
				
			||||||
            '';
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      devShells = forAllSystems (
 | 
					      devShells = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
        let
 | 
					        let
 | 
				
			||||||
          inherit (self.packages.${system}) fortify fhs;
 | 
					          inherit (self.packages.${system}) fortify;
 | 
				
			||||||
          pkgs = nixpkgsFor.${system};
 | 
					          pkgs = nixpkgsFor.${system};
 | 
				
			||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          default = pkgs.mkShell {
 | 
					          default = pkgs.mkShell { buildInputs = fortify.targetPkgs; };
 | 
				
			||||||
            buildInputs =
 | 
					          withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
 | 
				
			||||||
              with pkgs;
 | 
					 | 
				
			||||||
              [
 | 
					 | 
				
			||||||
                go
 | 
					 | 
				
			||||||
                gcc
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
              # buildInputs
 | 
					 | 
				
			||||||
              ++ (
 | 
					 | 
				
			||||||
                with pkgsStatic;
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                  musl
 | 
					 | 
				
			||||||
                  libffi
 | 
					 | 
				
			||||||
                  libseccomp
 | 
					 | 
				
			||||||
                  acl
 | 
					 | 
				
			||||||
                  wayland
 | 
					 | 
				
			||||||
                  wayland-protocols
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                ++ (with xorg; [
 | 
					 | 
				
			||||||
                  libxcb
 | 
					 | 
				
			||||||
                  libXau
 | 
					 | 
				
			||||||
                  libXdmcp
 | 
					 | 
				
			||||||
                ])
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
              # nativeBuildInputs
 | 
					 | 
				
			||||||
              ++ [
 | 
					 | 
				
			||||||
                pkg-config
 | 
					 | 
				
			||||||
                wayland-scanner
 | 
					 | 
				
			||||||
                makeBinaryWrapper
 | 
					 | 
				
			||||||
              ];
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          fhs = fhs.env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          withPackage = nixpkgsFor.${system}.mkShell {
 | 
					 | 
				
			||||||
            buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          generateDoc =
 | 
					          generateDoc =
 | 
				
			||||||
            let
 | 
					            let
 | 
				
			||||||
              pkgs = nixpkgsFor.${system};
 | 
					 | 
				
			||||||
              inherit (pkgs) lib;
 | 
					              inherit (pkgs) lib;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              doc =
 | 
					              doc =
 | 
				
			||||||
@ -223,7 +163,7 @@
 | 
				
			|||||||
                    specialArgs = {
 | 
					                    specialArgs = {
 | 
				
			||||||
                      inherit pkgs;
 | 
					                      inherit pkgs;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    modules = [ ./options.nix ];
 | 
					                    modules = [ (import ./options.nix self.packages) ];
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
					                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
@ -233,7 +173,7 @@
 | 
				
			|||||||
                sed -i '/*Declared by:*/,+1 d' $out
 | 
					                sed -i '/*Declared by:*/,+1 d' $out
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
            nixpkgsFor.${system}.mkShell {
 | 
					            pkgs.mkShell {
 | 
				
			||||||
              shellHook = ''
 | 
					              shellHook = ''
 | 
				
			||||||
                exec cat ${docText} > options.md
 | 
					                exec cat ${docText} > options.md
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
package fst
 | 
					package fst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +18,7 @@ type App interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type SealedApp interface {
 | 
					type SealedApp interface {
 | 
				
			||||||
	// Run commits sealed system setup and starts the app process.
 | 
						// Run commits sealed system setup and starts the app process.
 | 
				
			||||||
	Run(ctx context.Context, rs *RunState) error
 | 
						Run(rs *RunState) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RunState stores the outcome of a call to [SealedApp.Run].
 | 
					// RunState stores the outcome of a call to [SealedApp.Run].
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ package fst
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,8 +14,11 @@ type Config struct {
 | 
				
			|||||||
	// passed to wayland security-context-v1 as application ID
 | 
						// passed to wayland security-context-v1 as application ID
 | 
				
			||||||
	// and used as part of defaults in dbus session proxy
 | 
						// and used as part of defaults in dbus session proxy
 | 
				
			||||||
	ID string `json:"id"`
 | 
						ID string `json:"id"`
 | 
				
			||||||
	// final argv, passed to init
 | 
					
 | 
				
			||||||
	Command []string `json:"command"`
 | 
						// absolute path to executable file
 | 
				
			||||||
 | 
						Path string `json:"path,omitempty"`
 | 
				
			||||||
 | 
						// final args passed to container init
 | 
				
			||||||
 | 
						Args []string `json:"args"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Confinement ConfinementConfig `json:"confinement"`
 | 
						Confinement ConfinementConfig `json:"confinement"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -26,13 +29,13 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	AppID int `json:"app_id"`
 | 
						AppID int `json:"app_id"`
 | 
				
			||||||
	// list of supplementary groups to inherit
 | 
						// list of supplementary groups to inherit
 | 
				
			||||||
	Groups []string `json:"groups"`
 | 
						Groups []string `json:"groups"`
 | 
				
			||||||
	// passwd username in the sandbox, defaults to passwd name of target uid or chronos
 | 
						// passwd username in container, defaults to passwd name of target uid or chronos
 | 
				
			||||||
	Username string `json:"username,omitempty"`
 | 
						Username string `json:"username,omitempty"`
 | 
				
			||||||
	// home directory in sandbox, empty for outer
 | 
						// home directory in container, empty for outer
 | 
				
			||||||
	Inner string `json:"home_inner"`
 | 
						Inner string `json:"home_inner"`
 | 
				
			||||||
	// home directory in init namespace
 | 
						// home directory in init namespace
 | 
				
			||||||
	Outer string `json:"home"`
 | 
						Outer string `json:"home"`
 | 
				
			||||||
	// bwrap sandbox confinement configuration
 | 
						// abstract sandbox configuration
 | 
				
			||||||
	Sandbox *SandboxConfig `json:"sandbox"`
 | 
						Sandbox *SandboxConfig `json:"sandbox"`
 | 
				
			||||||
	// extra acl ops, runs after everything else
 | 
						// extra acl ops, runs after everything else
 | 
				
			||||||
	ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
						ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
				
			||||||
@ -44,7 +47,7 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	// nil value makes session bus proxy assume built-in defaults
 | 
						// nil value makes session bus proxy assume built-in defaults
 | 
				
			||||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
						SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// system resources to expose to the sandbox
 | 
						// system resources to expose to the container
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablements `json:"enablements"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,24 +79,12 @@ func (e *ExtraPermConfig) String() string {
 | 
				
			|||||||
	return string(buf)
 | 
						return string(buf)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FilesystemConfig struct {
 | 
					 | 
				
			||||||
	// mount point in sandbox, same as src if empty
 | 
					 | 
				
			||||||
	Dst string `json:"dst,omitempty"`
 | 
					 | 
				
			||||||
	// host filesystem path to make available to sandbox
 | 
					 | 
				
			||||||
	Src string `json:"src"`
 | 
					 | 
				
			||||||
	// write access
 | 
					 | 
				
			||||||
	Write bool `json:"write,omitempty"`
 | 
					 | 
				
			||||||
	// device access
 | 
					 | 
				
			||||||
	Device bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// fail if mount fails
 | 
					 | 
				
			||||||
	Must bool `json:"require,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Template returns a fully populated instance of Config.
 | 
					// Template returns a fully populated instance of Config.
 | 
				
			||||||
func Template() *Config {
 | 
					func Template() *Config {
 | 
				
			||||||
	return &Config{
 | 
						return &Config{
 | 
				
			||||||
		ID: "org.chromium.Chromium",
 | 
							ID:   "org.chromium.Chromium",
 | 
				
			||||||
		Command: []string{
 | 
							Path: "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
							Args: []string{
 | 
				
			||||||
			"chromium",
 | 
								"chromium",
 | 
				
			||||||
			"--ignore-gpu-blocklist",
 | 
								"--ignore-gpu-blocklist",
 | 
				
			||||||
			"--disable-smooth-scrolling",
 | 
								"--disable-smooth-scrolling",
 | 
				
			||||||
@ -108,11 +99,13 @@ func Template() *Config {
 | 
				
			|||||||
			Inner:    "/var/lib/fortify",
 | 
								Inner:    "/var/lib/fortify",
 | 
				
			||||||
			Sandbox: &SandboxConfig{
 | 
								Sandbox: &SandboxConfig{
 | 
				
			||||||
				Hostname:      "localhost",
 | 
									Hostname:      "localhost",
 | 
				
			||||||
				UserNS:        true,
 | 
									Devel:         true,
 | 
				
			||||||
 | 
									Userns:        true,
 | 
				
			||||||
				Net:           true,
 | 
									Net:           true,
 | 
				
			||||||
				Dev:           true,
 | 
									Dev:           true,
 | 
				
			||||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
									Seccomp:       seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession:  true,
 | 
									Tty:           true,
 | 
				
			||||||
 | 
									Multiarch:     true,
 | 
				
			||||||
				MapRealUID:    true,
 | 
									MapRealUID:    true,
 | 
				
			||||||
				DirectWayland: false,
 | 
									DirectWayland: false,
 | 
				
			||||||
				// example API credentials pulled from Google Chrome
 | 
									// example API credentials pulled from Google Chrome
 | 
				
			||||||
@ -131,10 +124,10 @@ func Template() *Config {
 | 
				
			|||||||
						Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
											Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
				
			||||||
					{Src: "/dev/dri", Device: true},
 | 
										{Src: "/dev/dri", Device: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
									Link:    [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
				
			||||||
				Etc:      "/etc",
 | 
									Etc:     "/etc",
 | 
				
			||||||
				AutoEtc:  true,
 | 
									AutoEtc: true,
 | 
				
			||||||
				Override: []string{"/var/run/nscd"},
 | 
									Cover:   []string{"/var/run/nscd"},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			ExtraPerms: []*ExtraPermConfig{
 | 
								ExtraPerms: []*ExtraPermConfig{
 | 
				
			||||||
				{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
									{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										269
									
								
								fst/sandbox.go
									
									
									
									
									
								
							
							
						
						
									
										269
									
								
								fst/sandbox.go
									
									
									
									
									
								
							@ -4,125 +4,149 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxConfig describes resources made available to the sandbox.
 | 
					// SandboxConfig describes resources made available to the sandbox.
 | 
				
			||||||
type SandboxConfig struct {
 | 
					type (
 | 
				
			||||||
	// unix hostname within sandbox
 | 
						SandboxConfig struct {
 | 
				
			||||||
	Hostname string `json:"hostname,omitempty"`
 | 
							// container hostname
 | 
				
			||||||
	// allow userns within sandbox
 | 
							Hostname string `json:"hostname,omitempty"`
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
					 | 
				
			||||||
	// share net namespace
 | 
					 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
					 | 
				
			||||||
	// share all devices
 | 
					 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// seccomp syscall filter policy
 | 
					 | 
				
			||||||
	Syscall *bwrap.SyscallPolicy `json:"syscall"`
 | 
					 | 
				
			||||||
	// do not run in new session
 | 
					 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
					 | 
				
			||||||
	// map target user uid to privileged user uid in the user namespace
 | 
					 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid"`
 | 
					 | 
				
			||||||
	// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
					 | 
				
			||||||
	// and the bare socket is mounted to the sandbox
 | 
					 | 
				
			||||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// final environment variables
 | 
							// extra seccomp flags
 | 
				
			||||||
	Env map[string]string `json:"env"`
 | 
							Seccomp seccomp.SyscallOpts `json:"seccomp"`
 | 
				
			||||||
	// sandbox host filesystem access
 | 
							// allow ptrace and friends
 | 
				
			||||||
	Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
							Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
	// symlinks created inside the sandbox
 | 
							// allow userns creation in container
 | 
				
			||||||
	Link [][2]string `json:"symlink"`
 | 
							Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// read-only /etc directory
 | 
							// share host net namespace
 | 
				
			||||||
	Etc string `json:"etc,omitempty"`
 | 
							Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// automatically set up /etc symlinks
 | 
							// expose main process tty
 | 
				
			||||||
	AutoEtc bool `json:"auto_etc"`
 | 
							Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// mount tmpfs over these paths,
 | 
							// allow multiarch
 | 
				
			||||||
	// runs right before [ConfinementConfig.ExtraPerms]
 | 
							Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	Override []string `json:"override"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
 | 
							// initial process environment variables
 | 
				
			||||||
type SandboxSys interface {
 | 
							Env map[string]string `json:"env"`
 | 
				
			||||||
	Geteuid() int
 | 
							// map target user uid to privileged user uid in the user namespace
 | 
				
			||||||
	Paths() Paths
 | 
							MapRealUID bool `json:"map_real_uid"`
 | 
				
			||||||
	ReadDir(name string) ([]fs.DirEntry, error)
 | 
					 | 
				
			||||||
	EvalSymlinks(path string) (string, error)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Println(v ...any)
 | 
							// expose all devices
 | 
				
			||||||
	Printf(format string, v ...any)
 | 
							Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
}
 | 
							// container host filesystem bind mounts
 | 
				
			||||||
 | 
							Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
				
			||||||
 | 
							// create symlinks inside container filesystem
 | 
				
			||||||
 | 
							Link [][2]string `json:"symlink"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
 | 
							// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
				
			||||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
 | 
							// and the bare socket is mounted to the sandbox
 | 
				
			||||||
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
							DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// read-only /etc directory
 | 
				
			||||||
 | 
							Etc string `json:"etc,omitempty"`
 | 
				
			||||||
 | 
							// automatically set up /etc symlinks
 | 
				
			||||||
 | 
							AutoEtc bool `json:"auto_etc"`
 | 
				
			||||||
 | 
							// cover these paths or create them if they do not already exist
 | 
				
			||||||
 | 
							Cover []string `json:"cover"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
 | 
				
			||||||
 | 
						SandboxSys interface {
 | 
				
			||||||
 | 
							Getuid() int
 | 
				
			||||||
 | 
							Getgid() int
 | 
				
			||||||
 | 
							Paths() Paths
 | 
				
			||||||
 | 
							ReadDir(name string) ([]fs.DirEntry, error)
 | 
				
			||||||
 | 
							EvalSymlinks(path string) (string, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Println(v ...any)
 | 
				
			||||||
 | 
							Printf(format string, v ...any)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FilesystemConfig is a representation of [sandbox.BindMount].
 | 
				
			||||||
 | 
						FilesystemConfig struct {
 | 
				
			||||||
 | 
							// mount point in container, same as src if empty
 | 
				
			||||||
 | 
							Dst string `json:"dst,omitempty"`
 | 
				
			||||||
 | 
							// host filesystem path to make available to the container
 | 
				
			||||||
 | 
							Src string `json:"src"`
 | 
				
			||||||
 | 
							// do not mount filesystem read-only
 | 
				
			||||||
 | 
							Write bool `json:"write,omitempty"`
 | 
				
			||||||
 | 
							// do not disable device files
 | 
				
			||||||
 | 
							Device bool `json:"dev,omitempty"`
 | 
				
			||||||
 | 
							// fail if the bind mount cannot be established for any reason
 | 
				
			||||||
 | 
							Must bool `json:"require,omitempty"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToContainer initialises [sandbox.Params] via [SandboxConfig].
 | 
				
			||||||
 | 
					// Note that remaining container setup must be queued by the [App] implementation.
 | 
				
			||||||
 | 
					func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		return nil, errors.New("nil sandbox config")
 | 
							return nil, nil, syscall.EBADE
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.Syscall == nil {
 | 
						container := &sandbox.Params{
 | 
				
			||||||
		sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !s.MapRealUID {
 | 
					 | 
				
			||||||
		// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
 | 
					 | 
				
			||||||
		*uid = 65534
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// some programs fail to connect to dbus session running as a different uid, so a separate workaround
 | 
					 | 
				
			||||||
		// is introduced to map priv-side caller uid in namespace
 | 
					 | 
				
			||||||
		*uid = sys.Geteuid()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conf := (&bwrap.Config{
 | 
					 | 
				
			||||||
		Net:      s.Net,
 | 
					 | 
				
			||||||
		UserNS:   s.UserNS,
 | 
					 | 
				
			||||||
		UID:      uid,
 | 
					 | 
				
			||||||
		GID:      uid,
 | 
					 | 
				
			||||||
		Hostname: s.Hostname,
 | 
							Hostname: s.Hostname,
 | 
				
			||||||
		Clearenv: true,
 | 
							Ops:      new(sandbox.Ops),
 | 
				
			||||||
		SetEnv:   s.Env,
 | 
							Seccomp:  s.Seccomp,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/* this is only 4 KiB of memory on a 64-bit system,
 | 
						/* this is only 4 KiB of memory on a 64-bit system,
 | 
				
			||||||
		permissive defaults on NixOS results in around 100 entries
 | 
						permissive defaults on NixOS results in around 100 entries
 | 
				
			||||||
		so this capacity should eliminate copies for most setups */
 | 
						so this capacity should eliminate copies for most setups */
 | 
				
			||||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
						*container.Ops = slices.Grow(*container.Ops, 1<<8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Syscall:       s.Syscall,
 | 
						if s.Devel {
 | 
				
			||||||
		NewSession:    !s.NoNewSession,
 | 
							container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
		DieWithParent: true,
 | 
						}
 | 
				
			||||||
		AsInit:        true,
 | 
						if s.Userns {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowUserns
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Net {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowNet
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Tty {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowTTY
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// initialise unconditionally as Once cannot be justified
 | 
						if s.MapRealUID {
 | 
				
			||||||
		// for saving such a miniscule amount of memory
 | 
							/* some programs fail to connect to dbus session running as a different uid
 | 
				
			||||||
		Chmod: make(bwrap.ChmodConfig),
 | 
							so this workaround is introduced to map priv-side caller uid in container */
 | 
				
			||||||
	}).
 | 
							container.Uid = sys.Getuid()
 | 
				
			||||||
		Procfs("/proc").
 | 
							*uid = container.Uid
 | 
				
			||||||
		Tmpfs(Tmp, 4*1024)
 | 
							container.Gid = sys.Getgid()
 | 
				
			||||||
 | 
							*gid = container.Gid
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							*uid = sandbox.OverflowUid()
 | 
				
			||||||
 | 
							*gid = sandbox.OverflowGid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						container.
 | 
				
			||||||
 | 
							Proc("/proc").
 | 
				
			||||||
 | 
							Tmpfs(Tmp, 1<<12, 0755)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.Dev {
 | 
						if !s.Dev {
 | 
				
			||||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
							container.Dev("/dev").Mqueue("/dev/mqueue")
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
							container.Bind("/dev", "/dev", sandbox.BindDevice)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.AutoEtc {
 | 
						/* retrieve paths and hide them if they're made available in the sandbox;
 | 
				
			||||||
		if s.Etc == "" {
 | 
						this feature tries to improve user experience of permissive defaults, and
 | 
				
			||||||
			conf.Dir("/etc")
 | 
						to warn about issues in custom configuration; it is NOT a security feature
 | 
				
			||||||
		} else {
 | 
						and should not be treated as such, ALWAYS be careful with what you bind */
 | 
				
			||||||
			conf.Bind(s.Etc, "/etc")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// retrieve paths and hide them if they're made available in the sandbox
 | 
					 | 
				
			||||||
	var hidePaths []string
 | 
						var hidePaths []string
 | 
				
			||||||
	sc := sys.Paths()
 | 
						sc := sys.Paths()
 | 
				
			||||||
	hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
						hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
				
			||||||
	_, systemBusAddr := dbus.Address()
 | 
						_, systemBusAddr := dbus.Address()
 | 
				
			||||||
	if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
						if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// there is usually only one, do not preallocate
 | 
							// there is usually only one, do not preallocate
 | 
				
			||||||
		for _, entry := range entries {
 | 
							for _, entry := range entries {
 | 
				
			||||||
@ -148,7 +172,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
	hidePathMatch := make([]bool, len(hidePaths))
 | 
						hidePathMatch := make([]bool, len(hidePaths))
 | 
				
			||||||
	for i := range hidePaths {
 | 
						for i := range hidePaths {
 | 
				
			||||||
		if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
							if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -158,19 +182,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !path.IsAbs(c.Src) {
 | 
							if !path.IsAbs(c.Src) {
 | 
				
			||||||
			return nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
								return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		dest := c.Dst
 | 
							dest := c.Dst
 | 
				
			||||||
		if c.Dst == "" {
 | 
							if c.Dst == "" {
 | 
				
			||||||
			dest = c.Src
 | 
								dest = c.Src
 | 
				
			||||||
		} else if !path.IsAbs(dest) {
 | 
							} else if !path.IsAbs(dest) {
 | 
				
			||||||
			return nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
								return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		srcH := c.Src
 | 
							srcH := c.Src
 | 
				
			||||||
		if err := evalSymlinks(sys, &srcH); err != nil {
 | 
							if err := evalSymlinks(sys, &srcH); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for i := range hidePaths {
 | 
							for i := range hidePaths {
 | 
				
			||||||
@ -180,54 +204,71 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
								if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
				
			||||||
				return nil, err
 | 
									return nil, nil, err
 | 
				
			||||||
			} else if ok {
 | 
								} else if ok {
 | 
				
			||||||
				hidePathMatch[i] = true
 | 
									hidePathMatch[i] = true
 | 
				
			||||||
				sys.Printf("hiding paths from %q", c.Src)
 | 
									sys.Printf("hiding paths from %q", c.Src)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
 | 
							var flags int
 | 
				
			||||||
 | 
							if c.Write {
 | 
				
			||||||
 | 
								flags |= sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.Device {
 | 
				
			||||||
 | 
								flags |= sandbox.BindDevice | sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !c.Must {
 | 
				
			||||||
 | 
								flags |= sandbox.BindOptional
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.Bind(c.Src, dest, flags)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// hide marked paths before setting up shares
 | 
						// cover matched paths
 | 
				
			||||||
	for i, ok := range hidePathMatch {
 | 
						for i, ok := range hidePathMatch {
 | 
				
			||||||
		if ok {
 | 
							if ok {
 | 
				
			||||||
			conf.Tmpfs(hidePaths[i], 8192)
 | 
								container.Tmpfs(hidePaths[i], 1<<13, 0755)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, l := range s.Link {
 | 
						for _, l := range s.Link {
 | 
				
			||||||
		conf.Symlink(l[0], l[1])
 | 
							container.Link(l[0], l[1])
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.AutoEtc {
 | 
						// perf: this might work better if implemented as a setup op in container init
 | 
				
			||||||
		etc := s.Etc
 | 
						if !s.AutoEtc {
 | 
				
			||||||
		if etc == "" {
 | 
							if s.Etc != "" {
 | 
				
			||||||
			etc = "/etc"
 | 
								container.Bind(s.Etc, "/etc", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		conf.Bind(etc, Tmp+"/etc")
 | 
						} else {
 | 
				
			||||||
 | 
							etcPath := s.Etc
 | 
				
			||||||
 | 
							if etcPath == "" {
 | 
				
			||||||
 | 
								etcPath = "/etc"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.
 | 
				
			||||||
 | 
								Bind(etcPath, Tmp+"/etc", 0).
 | 
				
			||||||
 | 
								Mkdir("/etc", 0700)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
							// link host /etc contents to prevent dropping passwd/group bind mounts
 | 
				
			||||||
		if d, err := sys.ReadDir(etc); err != nil {
 | 
							if d, err := sys.ReadDir(etcPath); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			for _, ent := range d {
 | 
								for _, ent := range d {
 | 
				
			||||||
				name := ent.Name()
 | 
									n := ent.Name()
 | 
				
			||||||
				switch name {
 | 
									switch n {
 | 
				
			||||||
				case "passwd":
 | 
									case "passwd":
 | 
				
			||||||
				case "group":
 | 
									case "group":
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case "mtab":
 | 
									case "mtab":
 | 
				
			||||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
										container.Link("/proc/mounts", "/etc/"+n)
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
										container.Link(Tmp+"/etc/"+n, "/etc/"+n)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return conf, nil
 | 
						return container, maps.Clone(s.Env), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func evalSymlinks(sys SandboxSys, v *string) error {
 | 
					func evalSymlinks(sys SandboxSys, v *string) error {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -1,3 +1,3 @@
 | 
				
			|||||||
module git.gensokyo.uk/security/fortify
 | 
					module git.gensokyo.uk/security/fortify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.22
 | 
					go 1.23
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,7 +62,7 @@ func (h *helperContainer) Start() error {
 | 
				
			|||||||
		h.Env = append(h.Env, FortifyStatus+"=1")
 | 
							h.Env = append(h.Env, FortifyStatus+"=1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// stat is populated on fulfill
 | 
							// stat is populated on fulfill
 | 
				
			||||||
		h.Cancel = func() error { return h.stat.Close() }
 | 
							h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		h.Env = append(h.Env, FortifyStatus+"=0")
 | 
							h.Env = append(h.Env, FortifyStatus+"=0")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
@ -10,9 +11,10 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func New(os sys.State) (fst.App, error) {
 | 
					func New(ctx context.Context, os sys.State) (fst.App, error) {
 | 
				
			||||||
	a := new(app)
 | 
						a := new(app)
 | 
				
			||||||
	a.sys = os
 | 
						a.sys = os
 | 
				
			||||||
 | 
						a.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	id := new(fst.ID)
 | 
						id := new(fst.ID)
 | 
				
			||||||
	err := fst.NewAppID(id)
 | 
						err := fst.NewAppID(id)
 | 
				
			||||||
@ -21,8 +23,8 @@ func New(os sys.State) (fst.App, error) {
 | 
				
			|||||||
	return a, err
 | 
						return a, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustNew(os sys.State) fst.App {
 | 
					func MustNew(ctx context.Context, os sys.State) fst.App {
 | 
				
			||||||
	a, err := New(os)
 | 
						a, err := New(ctx, os)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatalf("cannot create app: %v", err)
 | 
							log.Fatalf("cannot create app: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -32,6 +34,7 @@ func MustNew(os sys.State) fst.App {
 | 
				
			|||||||
type app struct {
 | 
					type app struct {
 | 
				
			||||||
	id  *stringPair[fst.ID]
 | 
						id  *stringPair[fst.ID]
 | 
				
			||||||
	sys sys.State
 | 
						sys sys.State
 | 
				
			||||||
 | 
						ctx context.Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	*outcome
 | 
						*outcome
 | 
				
			||||||
	mu sync.RWMutex
 | 
						mu sync.RWMutex
 | 
				
			||||||
@ -71,7 +74,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	seal := new(outcome)
 | 
						seal := new(outcome)
 | 
				
			||||||
	seal.id = a.id
 | 
						seal.id = a.id
 | 
				
			||||||
	err := seal.finalise(a.sys, config)
 | 
						err := seal.finalise(a.ctx, a.sys, config)
 | 
				
			||||||
	if err == nil {
 | 
						if err == nil {
 | 
				
			||||||
		a.outcome = seal
 | 
							a.outcome = seal
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,20 +12,20 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos chromium direct wayland", new(stubNixOS),
 | 
							"nixos chromium direct wayland", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
								Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
									AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
				
			||||||
				Outer: "/var/lib/persist/module/fortify/0/1",
 | 
									Outer: "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				Sandbox: &fst.SandboxConfig{
 | 
									Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
					UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
										Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
				
			||||||
					Filesystem: []*fst.FilesystemConfig{
 | 
										Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
						{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
											{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
				
			||||||
						{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
											{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
				
			||||||
						{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
											{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
				
			||||||
						{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
											{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					Override: []string{"/var/run/nscd"},
 | 
										Cover: []string{"/var/run/nscd"},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				SystemBus: &dbus.Config{
 | 
									SystemBus: &dbus.Config{
 | 
				
			||||||
					Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
										Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
				
			||||||
@ -88,136 +88,133 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Uid:   1971,
 | 
				
			||||||
			UserNS:   true,
 | 
								Gid:   100,
 | 
				
			||||||
			Chdir:    "/var/lib/persist/module/fortify/0/1",
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
 | 
				
			||||||
			Clearenv: true,
 | 
								Dir:   "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
								Path:  "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
 | 
								Args:  []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
								Env: []string{
 | 
				
			||||||
				"HOME":                     "/var/lib/persist/module/fortify/0/1",
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/1971/pulse/native",
 | 
									"HOME=/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"PULSE_SERVER=unix:/run/user/1971/pulse/native",
 | 
				
			||||||
				"USER":                     "u0_a1",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"USER=u0_a1",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/1971",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
									"XDG_RUNTIME_DIR=/run/user/1971",
 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			NewSession:    true,
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(1971).SetGID(1971).
 | 
									Bind("/bin", "/bin", 0).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/usr/bin", "/usr/bin", 0).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/nix/store", "/nix/store", 0).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/run/current-system", "/run/current-system", 0).
 | 
				
			||||||
			Bind("/bin", "/bin").
 | 
									Bind("/sys/block", "/sys/block", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr/bin", "/usr/bin").
 | 
									Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/nix/store", "/nix/store").
 | 
									Bind("/sys/class", "/sys/class", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/run/current-system", "/run/current-system").
 | 
									Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/block", "/sys/block", true).
 | 
									Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/bus", "/sys/bus", true).
 | 
									Bind("/run/opengl-driver", "/run/opengl-driver", 0).
 | 
				
			||||||
			Bind("/sys/class", "/sys/class", true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/dev", "/sys/dev", true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/sys/devices", "/sys/devices", true).
 | 
									Mkdir("/etc", 0700).
 | 
				
			||||||
			Bind("/run/opengl-driver", "/run/opengl-driver").
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Tmpfs("/run/user/1971", 8388608, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8388608).
 | 
									Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
 | 
									Place("/etc/group", []byte("fortify:x:100:\n")).
 | 
				
			||||||
			Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
 | 
									Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
									Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			Bind("/tmp/fortify.1971/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/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,6 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos permissive defaults no enablements", new(stubNixOS),
 | 
							"nixos permissive defaults no enablements", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			Command: make([]string, 0),
 | 
					 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    0,
 | 
									AppID:    0,
 | 
				
			||||||
				Username: "chronos",
 | 
									Username: "chronos",
 | 
				
			||||||
@ -35,136 +34,132 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
								Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
								Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
								Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Clearenv: true,
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Args:  []string{"/run/current-system/sw/bin/zsh"},
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":              "/home/chronos",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":             "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":              "xterm-256color",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"USER":              "chronos",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":   "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS": "user",
 | 
								},
 | 
				
			||||||
				"XDG_SESSION_TYPE":  "tty"},
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Mkdir("/etc", 0700).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Tmpfs("/run/user/65534", 8388608, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			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),
 | 
							"nixos permissive defaults chromium", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
 | 
								Args: []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    9,
 | 
									AppID:    9,
 | 
				
			||||||
				Groups:   []string{"video"},
 | 
									Groups:   []string{"video"},
 | 
				
			||||||
@ -254,141 +249,136 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Clearenv: true,
 | 
								Args:  []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":                     "/home/chronos",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"PULSE_SERVER=unix:/run/user/65534/pulse/native",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/65534/pulse/native",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"USER":                     "chronos",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
					 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			DieWithParent: true,
 | 
									Proc("/proc").
 | 
				
			||||||
			AsInit:        true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Mkdir("/etc", 0700).
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Tmpfs("/run/user/65534", 8388608, 0755).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
									Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
									Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			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,7 +17,8 @@ type stubNixOS struct {
 | 
				
			|||||||
	usernameErr map[string]error
 | 
						usernameErr map[string]error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *stubNixOS) Geteuid() int                             { return 1971 }
 | 
					func (s *stubNixOS) Getuid() int                              { return 1971 }
 | 
				
			||||||
 | 
					func (s *stubNixOS) Getgid() int                              { return 100 }
 | 
				
			||||||
func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
					func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
				
			||||||
func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
					func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
				
			||||||
func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
					func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
				
			||||||
@ -54,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch file {
 | 
						switch file {
 | 
				
			||||||
	case "sudo":
 | 
						case "zsh":
 | 
				
			||||||
		return "/run/wrappers/bin/sudo", nil
 | 
							return "/run/current-system/sw/bin/zsh", nil
 | 
				
			||||||
	case "machinectl":
 | 
					 | 
				
			||||||
		return "/home/ophestra/.nix-profile/bin/machinectl", nil
 | 
					 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
							panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,19 +8,19 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type sealTestCase struct {
 | 
					type sealTestCase struct {
 | 
				
			||||||
	name      string
 | 
						name          string
 | 
				
			||||||
	os        sys.State
 | 
						os            sys.State
 | 
				
			||||||
	config    *fst.Config
 | 
						config        *fst.Config
 | 
				
			||||||
	id        fst.ID
 | 
						id            fst.ID
 | 
				
			||||||
	wantSys   *system.I
 | 
						wantSys       *system.I
 | 
				
			||||||
	wantBwrap *bwrap.Config
 | 
						wantContainer *sandbox.Params
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestApp(t *testing.T) {
 | 
					func TestApp(t *testing.T) {
 | 
				
			||||||
@ -30,15 +30,15 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			a := app.NewWithID(tc.id, tc.os)
 | 
								a := app.NewWithID(tc.id, tc.os)
 | 
				
			||||||
			var (
 | 
								var (
 | 
				
			||||||
				gotSys   *system.I
 | 
									gotSys       *system.I
 | 
				
			||||||
				gotBwrap *bwrap.Config
 | 
									gotContainer *sandbox.Params
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			if !t.Run("seal", func(t *testing.T) {
 | 
								if !t.Run("seal", func(t *testing.T) {
 | 
				
			||||||
				if sa, err := a.Seal(tc.config); err != nil {
 | 
									if sa, err := a.Seal(tc.config); err != nil {
 | 
				
			||||||
					t.Errorf("Seal: error = %v", err)
 | 
										t.Errorf("Seal: error = %v", err)
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
 | 
										gotSys, gotContainer = app.AppIParams(a, sa)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}) {
 | 
								}) {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("compare bwrap", func(t *testing.T) {
 | 
								t.Run("compare params", func(t *testing.T) {
 | 
				
			||||||
				if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
 | 
									if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
 | 
				
			||||||
					t.Errorf("seal: bwrap =\n%s\n, want\n%s",
 | 
										t.Errorf("seal: params =\n%s\n, want\n%s",
 | 
				
			||||||
						mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
 | 
											mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,8 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
 | 
				
			|||||||
	return a
 | 
						return a
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
 | 
					func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
 | 
				
			||||||
	v := a.(*app)
 | 
						v := a.(*app)
 | 
				
			||||||
	seal := sa.(*outcome)
 | 
						seal := sa.(*outcome)
 | 
				
			||||||
	if v.outcome != seal || v.id != seal.id {
 | 
						if v.outcome != seal || v.id != seal.id {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// used by the parent process
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TryArgv0 calls [Main] if the last element of argv0 is "init0".
 | 
					 | 
				
			||||||
func TryArgv0() {
 | 
					 | 
				
			||||||
	if len(os.Args) > 0 && path.Base(os.Args[0]) == "init0" {
 | 
					 | 
				
			||||||
		Main()
 | 
					 | 
				
			||||||
		internal.Exit(0)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,165 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_INIT"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// target full exec path
 | 
					 | 
				
			||||||
	Argv0 string
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -3,15 +3,12 @@ package app
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
						"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
@ -21,7 +18,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const shimSetupTimeout = 5 * time.Second
 | 
					const shimSetupTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
					func (seal *outcome) Run(rs *fst.RunState) error {
 | 
				
			||||||
	if !seal.f.CompareAndSwap(false, true) {
 | 
						if !seal.f.CompareAndSwap(false, true) {
 | 
				
			||||||
		// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
							// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
				
			||||||
		// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
							// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
				
			||||||
@ -37,33 +34,11 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	fmsg.Verbosef("version %s", internal.Version())
 | 
						fmsg.Verbosef("version %s", internal.Version())
 | 
				
			||||||
	fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
 | 
						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
 | 
							prepare/revert os state
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := seal.sys.Commit(ctx); err != nil {
 | 
						if err := seal.sys.Commit(seal.ctx); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	store := state.NewMulti(seal.runDirPath)
 | 
						store := state.NewMulti(seal.runDirPath)
 | 
				
			||||||
@ -137,7 +112,6 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	if startTime, err := cmd.Start(
 | 
						if startTime, err := cmd.Start(
 | 
				
			||||||
		seal.user.aid.String(),
 | 
							seal.user.aid.String(),
 | 
				
			||||||
		seal.user.supp,
 | 
							seal.user.supp,
 | 
				
			||||||
		seal.bwrapSync,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -145,7 +119,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		rs.Time = startTime
 | 
							rs.Time = startTime
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
 | 
						ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
@ -154,11 +128,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		cancel()
 | 
							cancel()
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := cmd.Serve(c, &shim.Payload{
 | 
						if err := cmd.Serve(ctx, &shim.Params{
 | 
				
			||||||
		Argv:  seal.command,
 | 
							Container: seal.container,
 | 
				
			||||||
		Exec:  shimExec,
 | 
							Home:      seal.user.data,
 | 
				
			||||||
		Bwrap: seal.container,
 | 
					 | 
				
			||||||
		Home:  seal.user.data,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Verbose: fmsg.Load(),
 | 
							Verbose: fmsg.Load(),
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
@ -199,18 +171,22 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	// this is reached when a fault makes an already running shim impossible to continue execution
 | 
						// this is reached when a fault makes an already running shim impossible to continue execution
 | 
				
			||||||
	// however a kill signal could not be delivered (should actually always happen like that since fsu)
 | 
						// however a kill signal could not be delivered (should actually always happen like that since fsu)
 | 
				
			||||||
	// the effects of this is similar to the alternative exit path and ensures shim death
 | 
						// the effects of this is similar to the alternative exit path and ensures shim death
 | 
				
			||||||
	case err := <-cmd.WaitFallback():
 | 
						case err := <-cmd.Fallback():
 | 
				
			||||||
		rs.ExitCode = 255
 | 
							rs.ExitCode = 255
 | 
				
			||||||
		log.Printf("cannot terminate shim on faulted setup: %v", err)
 | 
							log.Printf("cannot terminate shim on faulted setup: %v", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// alternative exit path relying on shim behaviour on monitor process exit
 | 
						// alternative exit path relying on shim behaviour on monitor process exit
 | 
				
			||||||
	case <-ctx.Done():
 | 
						case <-seal.ctx.Done():
 | 
				
			||||||
		fmsg.Verbose("alternative exit path selected")
 | 
							fmsg.Verbose("alternative exit path selected")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Resume()
 | 
						fmsg.Resume()
 | 
				
			||||||
 | 
						if seal.sync != nil {
 | 
				
			||||||
 | 
							if err := seal.sync.Close(); err != nil {
 | 
				
			||||||
 | 
								log.Printf("cannot close wayland security context: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if seal.dbusMsg != nil {
 | 
						if seal.dbusMsg != nil {
 | 
				
			||||||
		// dump dbus message buffer
 | 
					 | 
				
			||||||
		seal.dbusMsg()
 | 
							seal.dbusMsg()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,24 +2,28 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/wl"
 | 
						"git.gensokyo.uk/security/fortify/wl"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -65,19 +69,19 @@ type outcome struct {
 | 
				
			|||||||
	// copied from [sys.State] response
 | 
						// copied from [sys.State] response
 | 
				
			||||||
	runDirPath string
 | 
						runDirPath string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through from [fst.Config]
 | 
					 | 
				
			||||||
	command []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// initial [fst.Config] gob stream for state data;
 | 
						// initial [fst.Config] gob stream for state data;
 | 
				
			||||||
	// this is prepared ahead of time as config is mutated during seal creation
 | 
						// this is prepared ahead of time as config is clobbered during seal creation
 | 
				
			||||||
	ct io.WriterTo
 | 
						ct io.WriterTo
 | 
				
			||||||
	// dump dbus proxy message buffer
 | 
						// dump dbus proxy message buffer
 | 
				
			||||||
	dbusMsg func()
 | 
						dbusMsg func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user      fsuUser
 | 
						user fsuUser
 | 
				
			||||||
	sys       *system.I
 | 
						sys  *system.I
 | 
				
			||||||
	container *bwrap.Config
 | 
						ctx  context.Context
 | 
				
			||||||
	bwrapSync *os.File
 | 
					
 | 
				
			||||||
 | 
						container *sandbox.Params
 | 
				
			||||||
 | 
						env       map[string]string
 | 
				
			||||||
 | 
						sync      *os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	f atomic.Bool
 | 
						f atomic.Bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -100,7 +104,17 @@ type fsuUser struct {
 | 
				
			|||||||
	username string
 | 
						username string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
					func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
 | 
				
			||||||
 | 
						if seal.ctx != nil {
 | 
				
			||||||
 | 
							panic("finalise called twice")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						seal.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shellPath := "/bin/sh"
 | 
				
			||||||
 | 
						if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
 | 
				
			||||||
 | 
							shellPath = s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// encode initial configuration for state tracking
 | 
							// encode initial configuration for state tracking
 | 
				
			||||||
		ct := new(bytes.Buffer)
 | 
							ct := new(bytes.Buffer)
 | 
				
			||||||
@ -111,9 +125,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.ct = ct
 | 
							seal.ct = ct
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass through command slice; this value is never touched in the main process
 | 
					 | 
				
			||||||
	seal.command = config.Command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// allowed aid range 0 to 9999, this is checked again in fsu
 | 
						// allowed aid range 0 to 9999, this is checked again in fsu
 | 
				
			||||||
	if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
						if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
				
			||||||
		return fmsg.WrapError(ErrUser,
 | 
							return fmsg.WrapError(ErrUser,
 | 
				
			||||||
@ -167,12 +178,24 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if config.Confinement.Sandbox == nil {
 | 
						if config.Confinement.Sandbox == nil {
 | 
				
			||||||
		fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
							fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fsu clears the environment so resolve paths early
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								if len(config.Args) > 0 {
 | 
				
			||||||
 | 
									if p, err := sys.LookPath(config.Args[0]); err != nil {
 | 
				
			||||||
 | 
										return fmsg.WrapError(err, err.Error())
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										config.Path = p
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									config.Path = shellPath
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf := &fst.SandboxConfig{
 | 
							conf := &fst.SandboxConfig{
 | 
				
			||||||
			UserNS:       true,
 | 
								Userns:  true,
 | 
				
			||||||
			Net:          true,
 | 
								Net:     true,
 | 
				
			||||||
			Syscall:      new(bwrap.SyscallPolicy),
 | 
								Tty:     true,
 | 
				
			||||||
			NoNewSession: true,
 | 
								AutoEtc: true,
 | 
				
			||||||
			AutoEtc:      true,
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind entries in /
 | 
							// bind entries in /
 | 
				
			||||||
		if d, err := sys.ReadDir("/"); err != nil {
 | 
							if d, err := sys.ReadDir("/"); err != nil {
 | 
				
			||||||
@ -198,7 +221,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		// hide nscd from sandbox if present
 | 
							// hide nscd from sandbox if present
 | 
				
			||||||
		nscd := "/var/run/nscd"
 | 
							nscd := "/var/run/nscd"
 | 
				
			||||||
		if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
							if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
			conf.Override = append(conf.Override, nscd)
 | 
								conf.Cover = append(conf.Cover, nscd)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind GPU stuff
 | 
							// bind GPU stuff
 | 
				
			||||||
		if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
 | 
							if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
 | 
				
			||||||
@ -210,17 +233,29 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		config.Confinement.Sandbox = conf
 | 
							config.Confinement.Sandbox = conf
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var mapuid *stringPair[int]
 | 
						var mapuid, mapgid *stringPair[int]
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		var uid int
 | 
							var uid, gid int
 | 
				
			||||||
		var err error
 | 
							var err error
 | 
				
			||||||
		seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
 | 
							seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
									"cannot initialise container configuration:")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.EINVAL,
 | 
				
			||||||
 | 
									"invalid program path")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(config.Args) == 0 {
 | 
				
			||||||
 | 
								config.Args = []string{config.Path}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							seal.container.Path = config.Path
 | 
				
			||||||
 | 
							seal.container.Args = config.Args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mapuid = newInt(uid)
 | 
							mapuid = newInt(uid)
 | 
				
			||||||
		if seal.container.SetEnv == nil {
 | 
							mapgid = newInt(gid)
 | 
				
			||||||
			seal.container.SetEnv = make(map[string]string)
 | 
							if seal.env == nil {
 | 
				
			||||||
 | 
								seal.env = make(map[string]string)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -255,35 +290,27 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
						// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
				
			||||||
	innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
						innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
				
			||||||
	seal.container.Tmpfs("/run/user", 1*1024*1024)
 | 
						seal.container.Tmpfs("/run/user", 1<<12, 0755)
 | 
				
			||||||
	seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
 | 
						seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755)
 | 
				
			||||||
	seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
 | 
						seal.env[xdgRuntimeDir] = innerRuntimeDir
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionClass] = "user"
 | 
						seal.env[xdgSessionClass] = "user"
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionType] = "tty"
 | 
						seal.env[xdgSessionType] = "tty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// outer path for inner /tmp
 | 
						// outer path for inner /tmp
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
							tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
				
			||||||
		seal.sys.Ensure(tmpdir, 0700)
 | 
							seal.sys.Ensure(tmpdir, 0700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
				
			||||||
		tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
 | 
							tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
 | 
				
			||||||
		seal.sys.Ensure(tmpdirProc, 01700)
 | 
							seal.sys.Ensure(tmpdirInst, 01700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		seal.container.Bind(tmpdirProc, "/tmp", false, true)
 | 
							seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Passwd database
 | 
							Passwd database
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// look up shell
 | 
					 | 
				
			||||||
	sh := "/bin/sh"
 | 
					 | 
				
			||||||
	if s, ok := sys.LookupEnv(shell); ok {
 | 
					 | 
				
			||||||
		seal.container.SetEnv[shell] = s
 | 
					 | 
				
			||||||
		sh = s
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// bind home directory
 | 
					 | 
				
			||||||
	homeDir := "/var/empty"
 | 
						homeDir := "/var/empty"
 | 
				
			||||||
	if seal.user.home != "" {
 | 
						if seal.user.home != "" {
 | 
				
			||||||
		homeDir = seal.user.home
 | 
							homeDir = seal.user.home
 | 
				
			||||||
@ -292,27 +319,25 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if seal.user.username != "" {
 | 
						if seal.user.username != "" {
 | 
				
			||||||
		username = seal.user.username
 | 
							username = seal.user.username
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	seal.container.Bind(seal.user.data, homeDir, false, true)
 | 
						seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
 | 
				
			||||||
	seal.container.Chdir = homeDir
 | 
						seal.container.Dir = homeDir
 | 
				
			||||||
	seal.container.SetEnv["HOME"] = homeDir
 | 
						seal.env["HOME"] = homeDir
 | 
				
			||||||
	seal.container.SetEnv["USER"] = username
 | 
						seal.env["USER"] = username
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// generate /etc/passwd and /etc/group
 | 
						seal.container.Place("/etc/passwd",
 | 
				
			||||||
	seal.container.CopyBind("/etc/passwd",
 | 
							[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
 | 
				
			||||||
		[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
 | 
						seal.container.Place("/etc/group",
 | 
				
			||||||
	seal.container.CopyBind("/etc/group",
 | 
							[]byte("fortify:x:"+mapgid.String()+":\n"))
 | 
				
			||||||
		[]byte("fortify:x:"+mapuid.String()+":\n"))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Display servers
 | 
							Display servers
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass $TERM to launcher
 | 
						// pass $TERM for proper terminal I/O in shell
 | 
				
			||||||
	if t, ok := sys.LookupEnv(term); ok {
 | 
						if t, ok := sys.LookupEnv(term); ok {
 | 
				
			||||||
		seal.container.SetEnv[term] = t
 | 
							seal.env[term] = t
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up wayland
 | 
					 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EWayland) {
 | 
						if config.Confinement.Enablements.Has(system.EWayland) {
 | 
				
			||||||
		// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
							// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
				
			||||||
		var socketPath string
 | 
							var socketPath string
 | 
				
			||||||
@ -326,7 +351,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
							innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
				
			||||||
		seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
 | 
							seal.env[wl.WaylandDisplay] = wl.FallbackName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
							if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
				
			||||||
			socketDir := path.Join(sc.SharePath, "wayland")
 | 
								socketDir := path.Join(sc.SharePath, "wayland")
 | 
				
			||||||
@ -337,25 +362,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
				// use instance ID in case app id is not set
 | 
									// use instance ID in case app id is not set
 | 
				
			||||||
				appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
									appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
 | 
								seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
 | 
				
			||||||
			seal.container.Bind(outerPath, innerPath)
 | 
								seal.container.Bind(outerPath, innerPath, 0)
 | 
				
			||||||
		} else { // bind mount wayland socket (insecure)
 | 
							} else { // bind mount wayland socket (insecure)
 | 
				
			||||||
			fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
								fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
				
			||||||
			seal.container.Bind(socketPath, innerPath)
 | 
								seal.container.Bind(socketPath, innerPath, 0)
 | 
				
			||||||
			seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
								seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up X11
 | 
					 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EX11) {
 | 
						if config.Confinement.Enablements.Has(system.EX11) {
 | 
				
			||||||
		// discover X11 and grant user permission via the `ChangeHosts` command
 | 
					 | 
				
			||||||
		if d, ok := sys.LookupEnv(display); !ok {
 | 
							if d, ok := sys.LookupEnv(display); !ok {
 | 
				
			||||||
			return fmsg.WrapError(ErrXDisplay,
 | 
								return fmsg.WrapError(ErrXDisplay,
 | 
				
			||||||
				"DISPLAY is not set")
 | 
									"DISPLAY is not set")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
								seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
				
			||||||
			seal.container.SetEnv[display] = d
 | 
								seal.env[display] = d
 | 
				
			||||||
			seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
 | 
								seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -396,8 +419,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
							innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
				
			||||||
		innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
							innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
				
			||||||
		seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
							seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
				
			||||||
		seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
 | 
							seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
 | 
				
			||||||
		seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
 | 
							seal.env[pulseServer] = "unix:" + innerPulseSocket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// publish current user's pulse cookie for target user
 | 
							// publish current user's pulse cookie for target user
 | 
				
			||||||
		if src, err := discoverPulseCookie(sys); err != nil {
 | 
							if src, err := discoverPulseCookie(sys); err != nil {
 | 
				
			||||||
@ -405,9 +428,9 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
			fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
								fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
								innerDst := fst.Tmp + "/pulse-cookie"
 | 
				
			||||||
			seal.container.SetEnv[pulseCookie] = innerDst
 | 
								seal.env[pulseCookie] = innerDst
 | 
				
			||||||
			payload := new([]byte)
 | 
								var payload *[]byte
 | 
				
			||||||
			seal.container.CopyBindRef(innerDst, &payload)
 | 
								seal.container.PlaceP(innerDst, &payload)
 | 
				
			||||||
			seal.sys.CopyFile(payload, src, 256, 256)
 | 
								seal.sys.CopyFile(payload, src, 256, 256)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -437,13 +460,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// share proxy sockets
 | 
							// share proxy sockets
 | 
				
			||||||
		sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
							sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
				
			||||||
		seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
							seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
				
			||||||
		seal.container.Bind(sessionPath, sessionInner)
 | 
							seal.container.Bind(sessionPath, sessionInner, 0)
 | 
				
			||||||
		seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
							seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
				
			||||||
		if config.Confinement.SystemBus != nil {
 | 
							if config.Confinement.SystemBus != nil {
 | 
				
			||||||
			systemInner := "/run/dbus/system_bus_socket"
 | 
								systemInner := "/run/dbus/system_bus_socket"
 | 
				
			||||||
			seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
								seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
				
			||||||
			seal.container.Bind(systemPath, systemInner)
 | 
								seal.container.Bind(systemPath, systemInner, 0)
 | 
				
			||||||
			seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
								seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -452,9 +475,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		Miscellaneous
 | 
							Miscellaneous
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// queue overriding tmpfs at the end of seal.container.Filesystem
 | 
						for _, dest := range config.Confinement.Sandbox.Cover {
 | 
				
			||||||
	for _, dest := range config.Confinement.Sandbox.Override {
 | 
							seal.container.Tmpfs(dest, 1<<13, 0755)
 | 
				
			||||||
		seal.container.Tmpfs(dest, 8*1024)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// append ExtraPerms last
 | 
						// append ExtraPerms last
 | 
				
			||||||
@ -480,12 +502,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
							seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// mount fortify in sandbox for init
 | 
						// flatten and sort env for deterministic behaviour
 | 
				
			||||||
	seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
 | 
						seal.container.Env = make([]string, 0, len(seal.env))
 | 
				
			||||||
	seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init0"))
 | 
						maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
 | 
				
			||||||
 | 
						slices.Sort(seal.container.Env)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
 | 
						fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
 | 
				
			||||||
		seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
 | 
							seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,18 +7,26 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"os/signal"
 | 
						"os/signal"
 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"syscall"
 | 
						"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"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/init0"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
						"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
 | 
					// everything beyond this point runs as unconstrained target user
 | 
				
			||||||
// proceed with caution!
 | 
					// proceed with caution!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,17 +35,15 @@ func Main() {
 | 
				
			|||||||
	// USE WITH CAUTION
 | 
						// USE WITH CAUTION
 | 
				
			||||||
	fmsg.Prepare("shim")
 | 
						fmsg.Prepare("shim")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// setting this prevents ptrace
 | 
					 | 
				
			||||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// receive setup payload
 | 
					 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		payload    Payload
 | 
							params     Params
 | 
				
			||||||
		closeSetup func() error
 | 
							closeSetup func() error
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
 | 
						if f, err := sandbox.Receive(Env, ¶ms, nil); err != nil {
 | 
				
			||||||
		if errors.Is(err, sandbox.ErrInvalid) {
 | 
							if errors.Is(err, sandbox.ErrInvalid) {
 | 
				
			||||||
			log.Fatal("invalid config descriptor")
 | 
								log.Fatal("invalid config descriptor")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -45,32 +51,26 @@ func Main() {
 | 
				
			|||||||
			log.Fatal("FORTIFY_SHIM not set")
 | 
								log.Fatal("FORTIFY_SHIM not set")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Fatalf("cannot decode shim setup payload: %v", err)
 | 
							log.Fatalf("cannot receive shim setup params: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		internal.InstallFmsg(payload.Verbose)
 | 
							internal.InstallFmsg(params.Verbose)
 | 
				
			||||||
		closeSetup = f
 | 
							closeSetup = f
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if payload.Bwrap == nil {
 | 
						if params.Container == nil || params.Container.Ops == nil {
 | 
				
			||||||
		log.Fatal("bwrap config not supplied")
 | 
							log.Fatal("invalid container params")
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// restore bwrap sync fd
 | 
					 | 
				
			||||||
	var syncFd *os.File
 | 
					 | 
				
			||||||
	if payload.Sync != nil {
 | 
					 | 
				
			||||||
		syncFd = os.NewFile(*payload.Sync, "sync")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// close setup socket
 | 
						// close setup socket
 | 
				
			||||||
	if err := closeSetup(); err != nil {
 | 
						if err := closeSetup(); err != nil {
 | 
				
			||||||
		log.Println("cannot close setup pipe:", err)
 | 
							log.Printf("cannot close setup pipe: %v", err)
 | 
				
			||||||
		// not fatal
 | 
							// not fatal
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ensure home directory as target user
 | 
						// ensure home directory as target user
 | 
				
			||||||
	if s, err := os.Stat(payload.Home); err != nil {
 | 
						if s, err := os.Stat(params.Home); err != nil {
 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
			if err = os.Mkdir(payload.Home, 0700); err != nil {
 | 
								if err = os.Mkdir(params.Home, 0700); err != nil {
 | 
				
			||||||
				log.Fatalf("cannot create home directory: %v", err)
 | 
									log.Fatalf("cannot create home directory: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
@ -79,72 +79,37 @@ func Main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// home directory is created, proceed
 | 
							// home directory is created, proceed
 | 
				
			||||||
	} else if !s.IsDir() {
 | 
						} else if !s.IsDir() {
 | 
				
			||||||
		log.Fatalf("data path %q is not a directory", payload.Home)
 | 
							log.Fatalf("path %q is not a directory", params.Home)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var ic init0.Payload
 | 
						var name string
 | 
				
			||||||
 | 
						if len(params.Container.Args) > 0 {
 | 
				
			||||||
	// resolve argv0
 | 
							name = params.Container.Args[0]
 | 
				
			||||||
	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)
 | 
						ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
				
			||||||
	defer stop() // unreachable
 | 
						defer stop() // unreachable
 | 
				
			||||||
	if b, err := helper.NewBwrap(
 | 
						container := sandbox.New(ctx, name)
 | 
				
			||||||
		ctx, path.Join(fst.Tmp, "sbin/init0"),
 | 
						container.Params = *params.Container
 | 
				
			||||||
		nil, false,
 | 
						container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
		func(int, int) []string { return make([]string, 0) },
 | 
						container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
 | 
				
			||||||
		func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
 | 
						container.WaitDelay = 2 * time.Second
 | 
				
			||||||
		extraFiles,
 | 
					
 | 
				
			||||||
		conf, syncFd,
 | 
						if err := container.Start(); err != nil {
 | 
				
			||||||
	); err != nil {
 | 
							fmsg.PrintBaseError(err, "cannot start container:")
 | 
				
			||||||
		log.Fatalf("malformed sandbox config: %v", err)
 | 
							os.Exit(1)
 | 
				
			||||||
	} else {
 | 
						}
 | 
				
			||||||
		// run and pass through exit code
 | 
						if err := container.Serve(); err != nil {
 | 
				
			||||||
		if err = b.Start(); err != nil {
 | 
							fmsg.PrintBaseError(err, "cannot configure container:")
 | 
				
			||||||
			log.Fatalf("cannot start target process: %v", err)
 | 
						}
 | 
				
			||||||
		} else if err = b.Wait(); err != nil {
 | 
						if err := container.Wait(); err != nil {
 | 
				
			||||||
			var exitError *exec.ExitError
 | 
							var exitError *exec.ExitError
 | 
				
			||||||
			if !errors.As(err, &exitError) {
 | 
							if !errors.As(err, &exitError) {
 | 
				
			||||||
				log.Printf("wait: %v", err)
 | 
								if errors.Is(err, context.Canceled) {
 | 
				
			||||||
				internal.Exit(127)
 | 
									os.Exit(2)
 | 
				
			||||||
				panic("unreachable")
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			internal.Exit(exitError.ExitCode())
 | 
								log.Printf("wait: %v", err)
 | 
				
			||||||
			panic("unreachable")
 | 
								os.Exit(127)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							os.Exit(exitError.ExitCode())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_SHIM"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
	// bwrap, target full exec path
 | 
					 | 
				
			||||||
	Exec [2]string
 | 
					 | 
				
			||||||
	// bwrap config
 | 
					 | 
				
			||||||
	Bwrap *bwrap.Config
 | 
					 | 
				
			||||||
	// path to outer home directory
 | 
					 | 
				
			||||||
	Home string
 | 
					 | 
				
			||||||
	// sync fd
 | 
					 | 
				
			||||||
	Sync *uintptr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -8,9 +8,9 @@ import (
 | 
				
			|||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
@ -25,10 +25,11 @@ type Shim struct {
 | 
				
			|||||||
	killFallback chan error
 | 
						killFallback chan error
 | 
				
			||||||
	// monitor to shim encoder
 | 
						// monitor to shim encoder
 | 
				
			||||||
	encoder *gob.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 {
 | 
					func (s *Shim) String() string {
 | 
				
			||||||
	if s.cmd == nil {
 | 
						if s.cmd == nil {
 | 
				
			||||||
		return "(unused shim manager)"
 | 
							return "(unused shim manager)"
 | 
				
			||||||
@ -36,21 +37,9 @@ func (s *Shim) String() string {
 | 
				
			|||||||
	return s.cmd.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(
 | 
					func (s *Shim) Start(
 | 
				
			||||||
	// string representation of application id
 | 
					 | 
				
			||||||
	aid string,
 | 
						aid string,
 | 
				
			||||||
	// string representation of supplementary group ids
 | 
					 | 
				
			||||||
	supp []string,
 | 
						supp []string,
 | 
				
			||||||
	// bwrap --sync-fd
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) (*time.Time, error) {
 | 
					) (*time.Time, error) {
 | 
				
			||||||
	// prepare user switcher invocation
 | 
						// prepare user switcher invocation
 | 
				
			||||||
	fsuPath := internal.MustFsuPath()
 | 
						fsuPath := internal.MustFsuPath()
 | 
				
			||||||
@ -76,12 +65,6 @@ func (s *Shim) Start(
 | 
				
			|||||||
	s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
						s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
	s.cmd.Dir = "/"
 | 
						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)
 | 
						fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
				
			||||||
	// withhold messages to stderr
 | 
						// withhold messages to stderr
 | 
				
			||||||
	fmsg.Suspend()
 | 
						fmsg.Suspend()
 | 
				
			||||||
@ -90,10 +73,11 @@ func (s *Shim) Start(
 | 
				
			|||||||
			"cannot start fsu:")
 | 
								"cannot start fsu:")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	startTime := time.Now().UTC()
 | 
						startTime := time.Now().UTC()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &startTime, nil
 | 
						return &startTime, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
					func (s *Shim) Serve(ctx context.Context, params *Params) error {
 | 
				
			||||||
	// kill shim if something goes wrong and an error is returned
 | 
						// kill shim if something goes wrong and an error is returned
 | 
				
			||||||
	s.killFallback = make(chan error, 1)
 | 
						s.killFallback = make(chan error, 1)
 | 
				
			||||||
	killShim := func() {
 | 
						killShim := func() {
 | 
				
			||||||
@ -103,9 +87,8 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer func() { killShim() }()
 | 
						defer func() { killShim() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	payload.Sync = s.sync
 | 
					 | 
				
			||||||
	encodeErr := make(chan error)
 | 
						encodeErr := make(chan error)
 | 
				
			||||||
	go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
						go func() { encodeErr <- s.encoder.Encode(params) }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	select {
 | 
						select {
 | 
				
			||||||
	// encode return indicates setup completion
 | 
						// encode return indicates setup completion
 | 
				
			||||||
@ -121,11 +104,11 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
				
			|||||||
	case <-ctx.Done():
 | 
						case <-ctx.Done():
 | 
				
			||||||
		err := ctx.Err()
 | 
							err := ctx.Err()
 | 
				
			||||||
		if errors.Is(err, context.Canceled) {
 | 
							if errors.Is(err, context.Canceled) {
 | 
				
			||||||
			return fmsg.WrapError(errors.New("shim setup canceled"),
 | 
								return fmsg.WrapError(syscall.ECANCELED,
 | 
				
			||||||
				"shim setup canceled")
 | 
									"shim setup canceled")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if errors.Is(err, context.DeadlineExceeded) {
 | 
							if errors.Is(err, context.DeadlineExceeded) {
 | 
				
			||||||
			return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
 | 
								return fmsg.WrapError(syscall.ETIMEDOUT,
 | 
				
			||||||
				"deadline exceeded waiting for shim")
 | 
									"deadline exceeded waiting for shim")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// unreachable
 | 
							// unreachable
 | 
				
			||||||
@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			slices.Sort(aids)
 | 
								slices.Sort(aids)
 | 
				
			||||||
			want := []int{0, 1}
 | 
								want := []int{0, 1}
 | 
				
			||||||
			if slices.Compare(aids, want) != 0 {
 | 
								if !slices.Equal(aids, want) {
 | 
				
			||||||
				t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
									t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,10 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// State provides safe interaction with operating system state.
 | 
					// State provides safe interaction with operating system state.
 | 
				
			||||||
type State interface {
 | 
					type State interface {
 | 
				
			||||||
	// Geteuid provides [os.Geteuid].
 | 
						// Getuid provides [os.Getuid].
 | 
				
			||||||
	Geteuid() int
 | 
						Getuid() int
 | 
				
			||||||
 | 
						// Getgid provides [os.Getgid].
 | 
				
			||||||
 | 
						Getgid() int
 | 
				
			||||||
	// LookupEnv provides [os.LookupEnv].
 | 
						// LookupEnv provides [os.LookupEnv].
 | 
				
			||||||
	LookupEnv(key string) (string, bool)
 | 
						LookupEnv(key string) (string, bool)
 | 
				
			||||||
	// TempDir provides [os.TempDir].
 | 
						// TempDir provides [os.TempDir].
 | 
				
			||||||
@ -47,7 +49,7 @@ type State interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CopyPaths is a generic implementation of [System.Paths].
 | 
					// CopyPaths is a generic implementation of [System.Paths].
 | 
				
			||||||
func CopyPaths(os State, v *fst.Paths) {
 | 
					func CopyPaths(os State, v *fst.Paths) {
 | 
				
			||||||
	v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
 | 
						v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
						fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,8 @@ type Std struct {
 | 
				
			|||||||
	uidMu sync.RWMutex
 | 
						uidMu sync.RWMutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Std) Geteuid() int                                 { return os.Geteuid() }
 | 
					func (s *Std) Getuid() int                                  { return os.Getuid() }
 | 
				
			||||||
 | 
					func (s *Std) Getgid() int                                  { return os.Getgid() }
 | 
				
			||||||
func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
					func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
				
			||||||
func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
					func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
				
			||||||
func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
					func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								main.go
									
									
									
									
									
								
							@ -20,7 +20,6 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"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/app/shim"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
@ -43,7 +42,6 @@ var std sys.State = new(sys.Std)
 | 
				
			|||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
	sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
	init0.TryArgv0()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
@ -76,9 +74,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
							Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// internal commands
 | 
					 | 
				
			||||||
	c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
						c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
 | 
				
			||||||
	c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
						c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
 | 
				
			||||||
		if len(args) < 1 {
 | 
							if len(args) < 1 {
 | 
				
			||||||
@ -87,10 +83,9 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// config extraArgs...
 | 
							// config extraArgs...
 | 
				
			||||||
		config := tryPath(args[0])
 | 
							config := tryPath(args[0])
 | 
				
			||||||
		config.Command = append(config.Command, args[1:]...)
 | 
							config.Args = append(config.Args, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// invoke app
 | 
							runApp(config)
 | 
				
			||||||
		runApp(app.MustNew(std), config)
 | 
					 | 
				
			||||||
		panic("unreachable")
 | 
							panic("unreachable")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -112,8 +107,8 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
							c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
				
			||||||
			// initialise config from flags
 | 
								// initialise config from flags
 | 
				
			||||||
			config := &fst.Config{
 | 
								config := &fst.Config{
 | 
				
			||||||
				ID:      fid,
 | 
									ID:   fid,
 | 
				
			||||||
				Command: args,
 | 
									Args: args,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if aid < 0 || aid > 9999 {
 | 
								if aid < 0 || aid > 9999 {
 | 
				
			||||||
@ -199,7 +194,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// invoke app
 | 
								// invoke app
 | 
				
			||||||
			runApp(app.MustNew(std), config)
 | 
								runApp(config)
 | 
				
			||||||
			panic("unreachable")
 | 
								panic("unreachable")
 | 
				
			||||||
		}).
 | 
							}).
 | 
				
			||||||
			Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
								Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
				
			||||||
@ -279,10 +274,11 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
	return c
 | 
						return c
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func runApp(a fst.App, config *fst.Config) {
 | 
					func runApp(config *fst.Config) {
 | 
				
			||||||
	ctx, stop := signal.NotifyContext(context.Background(),
 | 
						ctx, stop := signal.NotifyContext(context.Background(),
 | 
				
			||||||
		syscall.SIGINT, syscall.SIGTERM)
 | 
							syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
	defer stop() // unreachable
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
						a := app.MustNew(ctx, std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rs := new(fst.RunState)
 | 
						rs := new(fst.RunState)
 | 
				
			||||||
	if sa, err := a.Seal(config); err != nil {
 | 
						if sa, err := a.Seal(config); err != nil {
 | 
				
			||||||
@ -290,7 +286,7 @@ func runApp(a fst.App, config *fst.Config) {
 | 
				
			|||||||
		rs.ExitCode = 1
 | 
							rs.ExitCode = 1
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// this updates ExitCode
 | 
							// this updates ExitCode
 | 
				
			||||||
		app.PrintRunStateErr(rs, sa.Run(ctx, rs))
 | 
							app.PrintRunStateErr(rs, sa.Run(rs))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	internal.Exit(rs.ExitCode)
 | 
						internal.Exit(rs.ExitCode)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								nixos.nix
									
									
									
									
									
								
							@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					packages:
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
  pkgs,
 | 
					  pkgs,
 | 
				
			||||||
@ -26,7 +27,7 @@ let
 | 
				
			|||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  imports = [ ./options.nix ];
 | 
					  imports = [ (import ./options.nix packages) ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config = mkIf cfg.enable {
 | 
					  config = mkIf cfg.enable {
 | 
				
			||||||
    security.wrappers.fsu = {
 | 
					    security.wrappers.fsu = {
 | 
				
			||||||
@ -85,12 +86,11 @@ 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);
 | 
					                  enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
 | 
				
			||||||
                  conf = {
 | 
					                  conf = {
 | 
				
			||||||
                    inherit (app) id;
 | 
					                    inherit (app) id;
 | 
				
			||||||
                    command = [
 | 
					                    path = pkgs.writeScript "${app.name}-start" ''
 | 
				
			||||||
                      (pkgs.writeScript "${app.name}-start" ''
 | 
					                      #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
				
			||||||
                        #!${pkgs.zsh}${pkgs.zsh.shellPath}
 | 
					                      ${script}
 | 
				
			||||||
                        ${script}
 | 
					                    '';
 | 
				
			||||||
                      '')
 | 
					                    args = [ "${app.name}-start" ];
 | 
				
			||||||
                    ];
 | 
					 | 
				
			||||||
                    confinement = {
 | 
					                    confinement = {
 | 
				
			||||||
                      app_id = aid;
 | 
					                      app_id = aid;
 | 
				
			||||||
                      inherit (app) groups;
 | 
					                      inherit (app) groups;
 | 
				
			||||||
@ -98,17 +98,15 @@ in
 | 
				
			|||||||
                      home = getsubhome fid aid;
 | 
					                      home = getsubhome fid aid;
 | 
				
			||||||
                      sandbox = {
 | 
					                      sandbox = {
 | 
				
			||||||
                        inherit (app)
 | 
					                        inherit (app)
 | 
				
			||||||
 | 
					                          devel
 | 
				
			||||||
                          userns
 | 
					                          userns
 | 
				
			||||||
                          net
 | 
					                          net
 | 
				
			||||||
                          dev
 | 
					                          dev
 | 
				
			||||||
 | 
					                          tty
 | 
				
			||||||
 | 
					                          multiarch
 | 
				
			||||||
                          env
 | 
					                          env
 | 
				
			||||||
                          ;
 | 
					                          ;
 | 
				
			||||||
                        syscall = {
 | 
					 | 
				
			||||||
                          inherit (app) compat multiarch bluetooth;
 | 
					 | 
				
			||||||
                          deny_devel = !app.devel;
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                        map_real_uid = app.mapRealUid;
 | 
					                        map_real_uid = app.mapRealUid;
 | 
				
			||||||
                        no_new_session = app.tty;
 | 
					 | 
				
			||||||
                        direct_wayland = app.insecureWayland;
 | 
					                        direct_wayland = app.insecureWayland;
 | 
				
			||||||
                        filesystem =
 | 
					                        filesystem =
 | 
				
			||||||
                          let
 | 
					                          let
 | 
				
			||||||
@ -148,7 +146,7 @@ in
 | 
				
			|||||||
                          ]
 | 
					                          ]
 | 
				
			||||||
                          ++ app.extraPaths;
 | 
					                          ++ app.extraPaths;
 | 
				
			||||||
                        auto_etc = true;
 | 
					                        auto_etc = true;
 | 
				
			||||||
                        override = [ "/var/run/nscd" ];
 | 
					                        cover = [ "/var/run/nscd" ];
 | 
				
			||||||
                      };
 | 
					                      };
 | 
				
			||||||
                      inherit enablements;
 | 
					                      inherit enablements;
 | 
				
			||||||
                      inherit (dbusConfig) session_bus system_bus;
 | 
					                      inherit (dbusConfig) session_bus system_bus;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								options.nix
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								options.nix
									
									
									
									
									
								
							@ -1,17 +1,8 @@
 | 
				
			|||||||
 | 
					packages:
 | 
				
			||||||
{ lib, pkgs, ... }:
 | 
					{ lib, pkgs, ... }:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let
 | 
					let
 | 
				
			||||||
  inherit (lib) types mkOption mkEnableOption;
 | 
					  inherit (lib) types mkOption mkEnableOption;
 | 
				
			||||||
  fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					 | 
				
			||||||
    inherit (pkgs)
 | 
					 | 
				
			||||||
      bubblewrap
 | 
					 | 
				
			||||||
      xdg-dbus-proxy
 | 
					 | 
				
			||||||
      glibc
 | 
					 | 
				
			||||||
      zstd
 | 
					 | 
				
			||||||
      gnutar
 | 
					 | 
				
			||||||
      coreutils
 | 
					 | 
				
			||||||
      ;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -21,13 +12,13 @@ in
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      package = mkOption {
 | 
					      package = mkOption {
 | 
				
			||||||
        type = types.package;
 | 
					        type = types.package;
 | 
				
			||||||
        default = fortify;
 | 
					        default = packages.${pkgs.system}.fortify;
 | 
				
			||||||
        description = "The fortify package to use.";
 | 
					        description = "The fortify package to use.";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      fsuPackage = mkOption {
 | 
					      fsuPackage = mkOption {
 | 
				
			||||||
        type = types.package;
 | 
					        type = types.package;
 | 
				
			||||||
        default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
 | 
					        default = packages.${pkgs.system}.fsu;
 | 
				
			||||||
        description = "The fsu package to use.";
 | 
					        description = "The fsu package to use.";
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -157,21 +148,19 @@ in
 | 
				
			|||||||
                '';
 | 
					                '';
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              nix = mkEnableOption "nix daemon";
 | 
					              devel = mkEnableOption "debugging-related kernel interfaces";
 | 
				
			||||||
              userns = mkEnableOption "user namespace";
 | 
					              userns = mkEnableOption "user namespace creation";
 | 
				
			||||||
              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
					 | 
				
			||||||
              dev = mkEnableOption "access to all devices";
 | 
					 | 
				
			||||||
              tty = mkEnableOption "access to the controlling terminal";
 | 
					              tty = mkEnableOption "access to the controlling terminal";
 | 
				
			||||||
              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
					              multiarch = mkEnableOption "multiarch kernel-level support";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              net = mkEnableOption "network access" // {
 | 
					              net = mkEnableOption "network access" // {
 | 
				
			||||||
                default = true;
 | 
					                default = true;
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              compat = mkEnableOption "disable syscall filter extensions";
 | 
					              nix = mkEnableOption "nix daemon access";
 | 
				
			||||||
              devel = mkEnableOption "development kernel APIs";
 | 
					              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
				
			||||||
              multiarch = mkEnableOption "multiarch kernel support";
 | 
					              dev = mkEnableOption "access to all devices";
 | 
				
			||||||
              bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
 | 
					              insecureWayland = mkEnableOption "direct access to the Wayland socket";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              gpu = mkOption {
 | 
					              gpu = mkOption {
 | 
				
			||||||
                type = nullOr bool;
 | 
					                type = nullOr bool;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								package.nix
									
									
									
									
									
								
							@ -19,6 +19,13 @@
 | 
				
			|||||||
  gnutar,
 | 
					  gnutar,
 | 
				
			||||||
  coreutils,
 | 
					  coreutils,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # for passthru.buildInputs
 | 
				
			||||||
 | 
					  go,
 | 
				
			||||||
 | 
					  gcc,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # for check
 | 
				
			||||||
 | 
					  util-linux,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  glibc, # for ldd
 | 
					  glibc, # for ldd
 | 
				
			||||||
  withStatic ? stdenv.hostPlatform.isStatic,
 | 
					  withStatic ? stdenv.hostPlatform.isStatic,
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
@ -30,7 +37,7 @@ buildGoModule rec {
 | 
				
			|||||||
  src = builtins.path {
 | 
					  src = builtins.path {
 | 
				
			||||||
    name = "${pname}-src";
 | 
					    name = "${pname}-src";
 | 
				
			||||||
    path = lib.cleanSource ./.;
 | 
					    path = lib.cleanSource ./.;
 | 
				
			||||||
    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
					    filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  vendorHash = null;
 | 
					  vendorHash = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -108,4 +115,14 @@ buildGoModule rec {
 | 
				
			|||||||
          )
 | 
					          )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    '';
 | 
					    '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  passthru.targetPkgs =
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      go
 | 
				
			||||||
 | 
					      gcc
 | 
				
			||||||
 | 
					      xorg.xorgproto
 | 
				
			||||||
 | 
					      util-linux
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    ++ buildInputs
 | 
				
			||||||
 | 
					    ++ nativeBuildInputs;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								parse.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								parse.go
									
									
									
									
									
								
							@ -50,9 +50,12 @@ func tryPath(name string) (config *fst.Config) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func tryFd(name string) io.ReadCloser {
 | 
					func tryFd(name string) io.ReadCloser {
 | 
				
			||||||
	if v, err := strconv.Atoi(name); err != nil {
 | 
						if v, err := strconv.Atoi(name); err != nil {
 | 
				
			||||||
		fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
							if !errors.Is(err, strconv.ErrSyntax) {
 | 
				
			||||||
 | 
								fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							fmsg.Verbosef("trying config stream from %d", v)
 | 
				
			||||||
		fd := uintptr(v)
 | 
							fd := uintptr(v)
 | 
				
			||||||
		if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
 | 
							if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
 | 
				
			||||||
			if errors.Is(errno, syscall.EBADF) {
 | 
								if errors.Is(errno, syscall.EBADF) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								print.go
									
									
									
									
									
								
							@ -89,10 +89,10 @@ func printShowInstance(
 | 
				
			|||||||
				flags = append(flags, name)
 | 
									flags = append(flags, name)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		writeFlag("userns", sandbox.UserNS)
 | 
							writeFlag("userns", sandbox.Userns)
 | 
				
			||||||
		writeFlag("net", sandbox.Net)
 | 
							writeFlag("net", sandbox.Net)
 | 
				
			||||||
		writeFlag("dev", sandbox.Dev)
 | 
							writeFlag("dev", sandbox.Dev)
 | 
				
			||||||
		writeFlag("tty", sandbox.NoNewSession)
 | 
							writeFlag("tty", sandbox.Tty)
 | 
				
			||||||
		writeFlag("mapuid", sandbox.MapRealUID)
 | 
							writeFlag("mapuid", sandbox.MapRealUID)
 | 
				
			||||||
		writeFlag("directwl", sandbox.DirectWayland)
 | 
							writeFlag("directwl", sandbox.DirectWayland)
 | 
				
			||||||
		writeFlag("autoetc", sandbox.AutoEtc)
 | 
							writeFlag("autoetc", sandbox.AutoEtc)
 | 
				
			||||||
@ -107,14 +107,14 @@ func printShowInstance(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		t.Printf(" Etc:\t%s\n", etc)
 | 
							t.Printf(" Etc:\t%s\n", etc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if len(sandbox.Override) > 0 {
 | 
							if len(sandbox.Cover) > 0 {
 | 
				
			||||||
			t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
 | 
								t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Env           map[string]string   `json:"env"`
 | 
							// Env           map[string]string   `json:"env"`
 | 
				
			||||||
		// Link          [][2]string         `json:"symlink"`
 | 
							// Link          [][2]string         `json:"symlink"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
 | 
						t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
 | 
				
			||||||
	t.Printf("\n")
 | 
						t.Printf("\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !short {
 | 
						if !short {
 | 
				
			||||||
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
 | 
				
			|||||||
		)
 | 
							)
 | 
				
			||||||
		if e.Config != nil {
 | 
							if e.Config != nil {
 | 
				
			||||||
			es = e.Config.Confinement.Enablements.String()
 | 
								es = e.Config.Confinement.Enablements.String()
 | 
				
			||||||
			cs = fmt.Sprintf("%q", e.Config.Command)
 | 
								cs = fmt.Sprintf("%q", e.Config.Args)
 | 
				
			||||||
			as = strconv.Itoa(e.Config.Confinement.AppID)
 | 
								as = strconv.Itoa(e.Config.Confinement.AppID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
 | 
							t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
 | 
				
			||||||
 | 
				
			|||||||
@ -43,7 +43,7 @@ func Test_printShowInstance(t *testing.T) {
 | 
				
			|||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -127,7 +127,7 @@ App
 | 
				
			|||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -192,7 +192,8 @@ App
 | 
				
			|||||||
  "pid": 3735928559,
 | 
					  "pid": 3735928559,
 | 
				
			||||||
  "config": {
 | 
					  "config": {
 | 
				
			||||||
    "id": "org.chromium.Chromium",
 | 
					    "id": "org.chromium.Chromium",
 | 
				
			||||||
    "command": [
 | 
					    "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					    "args": [
 | 
				
			||||||
      "chromium",
 | 
					      "chromium",
 | 
				
			||||||
      "--ignore-gpu-blocklist",
 | 
					      "--ignore-gpu-blocklist",
 | 
				
			||||||
      "--disable-smooth-scrolling",
 | 
					      "--disable-smooth-scrolling",
 | 
				
			||||||
@ -209,24 +210,19 @@ App
 | 
				
			|||||||
      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
      "sandbox": {
 | 
					      "sandbox": {
 | 
				
			||||||
        "hostname": "localhost",
 | 
					        "hostname": "localhost",
 | 
				
			||||||
 | 
					        "seccomp": 32,
 | 
				
			||||||
 | 
					        "devel": true,
 | 
				
			||||||
        "userns": true,
 | 
					        "userns": true,
 | 
				
			||||||
        "net": true,
 | 
					        "net": true,
 | 
				
			||||||
        "dev": true,
 | 
					        "tty": true,
 | 
				
			||||||
        "syscall": {
 | 
					        "multiarch": true,
 | 
				
			||||||
          "compat": false,
 | 
					 | 
				
			||||||
          "deny_devel": true,
 | 
					 | 
				
			||||||
          "multiarch": true,
 | 
					 | 
				
			||||||
          "linux32": false,
 | 
					 | 
				
			||||||
          "can": false,
 | 
					 | 
				
			||||||
          "bluetooth": false
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "no_new_session": true,
 | 
					 | 
				
			||||||
        "map_real_uid": true,
 | 
					 | 
				
			||||||
        "env": {
 | 
					        "env": {
 | 
				
			||||||
          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "map_real_uid": true,
 | 
				
			||||||
 | 
					        "dev": true,
 | 
				
			||||||
        "filesystem": [
 | 
					        "filesystem": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "src": "/nix/store"
 | 
					            "src": "/nix/store"
 | 
				
			||||||
@ -259,7 +255,7 @@ App
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "etc": "/etc",
 | 
					        "etc": "/etc",
 | 
				
			||||||
        "auto_etc": true,
 | 
					        "auto_etc": true,
 | 
				
			||||||
        "override": [
 | 
					        "cover": [
 | 
				
			||||||
          "/var/run/nscd"
 | 
					          "/var/run/nscd"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -320,7 +316,8 @@ App
 | 
				
			|||||||
`},
 | 
					`},
 | 
				
			||||||
		{"json config", nil, fst.Template(), false, true, `{
 | 
							{"json config", nil, fst.Template(), false, true, `{
 | 
				
			||||||
  "id": "org.chromium.Chromium",
 | 
					  "id": "org.chromium.Chromium",
 | 
				
			||||||
  "command": [
 | 
					  "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					  "args": [
 | 
				
			||||||
    "chromium",
 | 
					    "chromium",
 | 
				
			||||||
    "--ignore-gpu-blocklist",
 | 
					    "--ignore-gpu-blocklist",
 | 
				
			||||||
    "--disable-smooth-scrolling",
 | 
					    "--disable-smooth-scrolling",
 | 
				
			||||||
@ -337,24 +334,19 @@ App
 | 
				
			|||||||
    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
    "sandbox": {
 | 
					    "sandbox": {
 | 
				
			||||||
      "hostname": "localhost",
 | 
					      "hostname": "localhost",
 | 
				
			||||||
 | 
					      "seccomp": 32,
 | 
				
			||||||
 | 
					      "devel": true,
 | 
				
			||||||
      "userns": true,
 | 
					      "userns": true,
 | 
				
			||||||
      "net": true,
 | 
					      "net": true,
 | 
				
			||||||
      "dev": true,
 | 
					      "tty": true,
 | 
				
			||||||
      "syscall": {
 | 
					      "multiarch": true,
 | 
				
			||||||
        "compat": false,
 | 
					 | 
				
			||||||
        "deny_devel": true,
 | 
					 | 
				
			||||||
        "multiarch": true,
 | 
					 | 
				
			||||||
        "linux32": false,
 | 
					 | 
				
			||||||
        "can": false,
 | 
					 | 
				
			||||||
        "bluetooth": false
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "no_new_session": true,
 | 
					 | 
				
			||||||
      "map_real_uid": true,
 | 
					 | 
				
			||||||
      "env": {
 | 
					      "env": {
 | 
				
			||||||
        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "map_real_uid": true,
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
      "filesystem": [
 | 
					      "filesystem": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "src": "/nix/store"
 | 
					          "src": "/nix/store"
 | 
				
			||||||
@ -387,7 +379,7 @@ App
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      "etc": "/etc",
 | 
					      "etc": "/etc",
 | 
				
			||||||
      "auto_etc": true,
 | 
					      "auto_etc": true,
 | 
				
			||||||
      "override": [
 | 
					      "cover": [
 | 
				
			||||||
        "/var/run/nscd"
 | 
					        "/var/run/nscd"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -506,7 +498,8 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
    "pid": 3735928559,
 | 
					    "pid": 3735928559,
 | 
				
			||||||
    "config": {
 | 
					    "config": {
 | 
				
			||||||
      "id": "org.chromium.Chromium",
 | 
					      "id": "org.chromium.Chromium",
 | 
				
			||||||
      "command": [
 | 
					      "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					      "args": [
 | 
				
			||||||
        "chromium",
 | 
					        "chromium",
 | 
				
			||||||
        "--ignore-gpu-blocklist",
 | 
					        "--ignore-gpu-blocklist",
 | 
				
			||||||
        "--disable-smooth-scrolling",
 | 
					        "--disable-smooth-scrolling",
 | 
				
			||||||
@ -523,24 +516,19 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
        "sandbox": {
 | 
					        "sandbox": {
 | 
				
			||||||
          "hostname": "localhost",
 | 
					          "hostname": "localhost",
 | 
				
			||||||
 | 
					          "seccomp": 32,
 | 
				
			||||||
 | 
					          "devel": true,
 | 
				
			||||||
          "userns": true,
 | 
					          "userns": true,
 | 
				
			||||||
          "net": true,
 | 
					          "net": true,
 | 
				
			||||||
          "dev": true,
 | 
					          "tty": true,
 | 
				
			||||||
          "syscall": {
 | 
					          "multiarch": true,
 | 
				
			||||||
            "compat": false,
 | 
					 | 
				
			||||||
            "deny_devel": true,
 | 
					 | 
				
			||||||
            "multiarch": true,
 | 
					 | 
				
			||||||
            "linux32": false,
 | 
					 | 
				
			||||||
            "can": false,
 | 
					 | 
				
			||||||
            "bluetooth": false
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "no_new_session": true,
 | 
					 | 
				
			||||||
          "map_real_uid": true,
 | 
					 | 
				
			||||||
          "env": {
 | 
					          "env": {
 | 
				
			||||||
            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "map_real_uid": true,
 | 
				
			||||||
 | 
					          "dev": true,
 | 
				
			||||||
          "filesystem": [
 | 
					          "filesystem": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "src": "/nix/store"
 | 
					              "src": "/nix/store"
 | 
				
			||||||
@ -573,7 +561,7 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
          "etc": "/etc",
 | 
					          "etc": "/etc",
 | 
				
			||||||
          "auto_etc": true,
 | 
					          "auto_etc": true,
 | 
				
			||||||
          "override": [
 | 
					          "cover": [
 | 
				
			||||||
            "/var/run/nscd"
 | 
					            "/var/run/nscd"
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +0,0 @@
 | 
				
			|||||||
package sandbox
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	PR_SET_NO_NEW_PRIVS = 0x26
 | 
					 | 
				
			||||||
	CAP_SYS_ADMIN       = 0x15
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -54,7 +54,6 @@ type (
 | 
				
			|||||||
		// with behaviour identical to its [exec.Cmd] counterpart.
 | 
							// with behaviour identical to its [exec.Cmd] counterpart.
 | 
				
			||||||
		ExtraFiles []*os.File
 | 
							ExtraFiles []*os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		InitParams
 | 
					 | 
				
			||||||
		// Custom [exec.Cmd] initialisation function.
 | 
							// Custom [exec.Cmd] initialisation function.
 | 
				
			||||||
		CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
							CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,14 +66,16 @@ type (
 | 
				
			|||||||
		Stdout io.Writer
 | 
							Stdout io.Writer
 | 
				
			||||||
		Stderr io.Writer
 | 
							Stderr io.Writer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Cancel    func() error
 | 
							Cancel    func(cmd *exec.Cmd) error
 | 
				
			||||||
		WaitDelay time.Duration
 | 
							WaitDelay time.Duration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		cmd *exec.Cmd
 | 
							cmd *exec.Cmd
 | 
				
			||||||
		ctx context.Context
 | 
							ctx context.Context
 | 
				
			||||||
 | 
							Params
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	InitParams struct {
 | 
						// Params holds container configuration and is safe to serialise.
 | 
				
			||||||
 | 
						Params struct {
 | 
				
			||||||
		// Working directory in the container.
 | 
							// Working directory in the container.
 | 
				
			||||||
		Dir string
 | 
							Dir string
 | 
				
			||||||
		// Initial process environment.
 | 
							// Initial process environment.
 | 
				
			||||||
@ -100,7 +101,9 @@ type (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	Ops []Op
 | 
						Ops []Op
 | 
				
			||||||
	Op  interface {
 | 
						Op  interface {
 | 
				
			||||||
		apply(params *InitParams) error
 | 
							early(params *Params) error
 | 
				
			||||||
 | 
							apply(params *Params) error
 | 
				
			||||||
 | 
							prefix() string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Is(op Op) bool
 | 
							Is(op Op) bool
 | 
				
			||||||
		fmt.Stringer
 | 
							fmt.Stringer
 | 
				
			||||||
@ -141,7 +144,12 @@ func (p *Container) Start() error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
 | 
						p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
 | 
				
			||||||
	p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay
 | 
						p.cmd.WaitDelay = p.WaitDelay
 | 
				
			||||||
 | 
						if p.Cancel != nil {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	p.cmd.Dir = "/"
 | 
						p.cmd.Dir = "/"
 | 
				
			||||||
	p.cmd.SysProcAttr = &syscall.SysProcAttr{
 | 
						p.cmd.SysProcAttr = &syscall.SysProcAttr{
 | 
				
			||||||
		Setsid:    p.Flags&FAllowTTY == 0,
 | 
							Setsid:    p.Flags&FAllowTTY == 0,
 | 
				
			||||||
@ -183,7 +191,11 @@ func (p *Container) Serve() error {
 | 
				
			|||||||
		panic("invalid serve")
 | 
							panic("invalid serve")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup := p.setup
 | 
				
			||||||
 | 
						p.setup = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if p.Path != "" && !path.IsAbs(p.Path) {
 | 
						if p.Path != "" && !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
		return msg.WrapErr(syscall.EINVAL,
 | 
							return msg.WrapErr(syscall.EINVAL,
 | 
				
			||||||
			fmt.Sprintf("invalid executable path %q", p.Path))
 | 
								fmt.Sprintf("invalid executable path %q", p.Path))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -192,6 +204,7 @@ func (p *Container) Serve() error {
 | 
				
			|||||||
		if p.name == "" {
 | 
							if p.name == "" {
 | 
				
			||||||
			p.Path = os.Getenv("SHELL")
 | 
								p.Path = os.Getenv("SHELL")
 | 
				
			||||||
			if !path.IsAbs(p.Path) {
 | 
								if !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
									p.cancel()
 | 
				
			||||||
				return msg.WrapErr(syscall.EBADE,
 | 
									return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
					"no command specified and $SHELL is invalid")
 | 
										"no command specified and $SHELL is invalid")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -199,23 +212,26 @@ func (p *Container) Serve() error {
 | 
				
			|||||||
		} else if path.IsAbs(p.name) {
 | 
							} else if path.IsAbs(p.name) {
 | 
				
			||||||
			p.Path = p.name
 | 
								p.Path = p.name
 | 
				
			||||||
		} else if v, err := exec.LookPath(p.name); err != nil {
 | 
							} else if v, err := exec.LookPath(p.name); err != nil {
 | 
				
			||||||
 | 
								p.cancel()
 | 
				
			||||||
			return msg.WrapErr(err, err.Error())
 | 
								return msg.WrapErr(err, err.Error())
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			p.Path = v
 | 
								p.Path = v
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	setup := p.setup
 | 
						err := setup.Encode(
 | 
				
			||||||
	p.setup = nil
 | 
					 | 
				
			||||||
	return setup.Encode(
 | 
					 | 
				
			||||||
		&initParams{
 | 
							&initParams{
 | 
				
			||||||
			p.InitParams,
 | 
								p.Params,
 | 
				
			||||||
			syscall.Getuid(),
 | 
								syscall.Getuid(),
 | 
				
			||||||
			syscall.Getgid(),
 | 
								syscall.Getgid(),
 | 
				
			||||||
			len(p.ExtraFiles),
 | 
								len(p.ExtraFiles),
 | 
				
			||||||
			msg.IsVerbose(),
 | 
								msg.IsVerbose(),
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
 | 
					func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
 | 
				
			||||||
@ -227,6 +243,6 @@ func (p *Container) String() string {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func New(ctx context.Context, name string, args ...string) *Container {
 | 
					func New(ctx context.Context, name string, args ...string) *Container {
 | 
				
			||||||
	return &Container{name: name, ctx: ctx,
 | 
						return &Container{name: name, ctx: ctx,
 | 
				
			||||||
		InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
							Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,10 +3,11 @@ package sandbox_test
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/gob"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@ -17,7 +18,12 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/sandbox"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	check "git.gensokyo.uk/security/fortify/test/sandbox"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ignore  = "\x00"
 | 
				
			||||||
 | 
						ignoreV = -1
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestContainer(t *testing.T) {
 | 
					func TestContainer(t *testing.T) {
 | 
				
			||||||
@ -33,7 +39,7 @@ func TestContainer(t *testing.T) {
 | 
				
			|||||||
		name  string
 | 
							name  string
 | 
				
			||||||
		flags sandbox.HardeningFlags
 | 
							flags sandbox.HardeningFlags
 | 
				
			||||||
		ops   *sandbox.Ops
 | 
							ops   *sandbox.Ops
 | 
				
			||||||
		mnt   []*check.Mntent
 | 
							mnt   []*vfs.MountInfoEntry
 | 
				
			||||||
		host  string
 | 
							host  string
 | 
				
			||||||
	}{
 | 
						}{
 | 
				
			||||||
		{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
 | 
							{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
 | 
				
			||||||
@ -42,21 +48,23 @@ func TestContainer(t *testing.T) {
 | 
				
			|||||||
		{"tmpfs", 0,
 | 
							{"tmpfs", 0,
 | 
				
			||||||
			new(sandbox.Ops).
 | 
								new(sandbox.Ops).
 | 
				
			||||||
				Tmpfs(fst.Tmp, 0, 0755),
 | 
									Tmpfs(fst.Tmp, 0, 0755),
 | 
				
			||||||
			[]*check.Mntent{
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
				{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
 | 
									e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
			}, "test-tmpfs"},
 | 
								}, "test-tmpfs"},
 | 
				
			||||||
		{"dev", sandbox.FAllowTTY, // go test output is not a tty
 | 
							{"dev", sandbox.FAllowTTY, // go test output is not a tty
 | 
				
			||||||
			new(sandbox.Ops).
 | 
								new(sandbox.Ops).
 | 
				
			||||||
				Dev("/dev"),
 | 
									Dev("/dev").
 | 
				
			||||||
			[]*check.Mntent{
 | 
									Mqueue("/dev/mqueue"),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
 | 
									e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
				{FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
 | 
									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"),
 | 
				
			||||||
			}, ""},
 | 
								}, ""},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,7 +73,7 @@ func TestContainer(t *testing.T) {
 | 
				
			|||||||
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
								ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
			defer cancel()
 | 
								defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			container := sandbox.New(ctx, os.Args[0], "-test.v",
 | 
								container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
 | 
				
			||||||
				"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
 | 
									"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
 | 
				
			||||||
			container.Uid = 1000
 | 
								container.Uid = 1000
 | 
				
			||||||
			container.Gid = 100
 | 
								container.Gid = 100
 | 
				
			||||||
@ -84,7 +92,10 @@ func TestContainer(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			container.
 | 
								container.
 | 
				
			||||||
				Tmpfs("/tmp", 0, 0755).
 | 
									Tmpfs("/tmp", 0, 0755).
 | 
				
			||||||
				Bind(os.Args[0], os.Args[0], 0)
 | 
									Bind(os.Args[0], os.Args[0], 0).
 | 
				
			||||||
 | 
									Mkdir("/usr/bin", 0755).
 | 
				
			||||||
 | 
									Link(os.Args[0], "/usr/bin/sandbox.test").
 | 
				
			||||||
 | 
									Place("/etc/hostname", []byte(container.Args[5]))
 | 
				
			||||||
			// in case test has cgo enabled
 | 
								// in case test has cgo enabled
 | 
				
			||||||
			var libPaths []string
 | 
								var libPaths []string
 | 
				
			||||||
			if entries, err := ldd.ExecFilter(ctx,
 | 
								if entries, err := ldd.ExecFilter(ctx,
 | 
				
			||||||
@ -99,25 +110,26 @@ func TestContainer(t *testing.T) {
 | 
				
			|||||||
			for _, name := range libPaths {
 | 
								for _, name := range libPaths {
 | 
				
			||||||
				container.Bind(name, name, 0)
 | 
									container.Bind(name, name, 0)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								// needs /proc to check mountinfo
 | 
				
			||||||
 | 
								container.Proc("/proc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			mnt := make([]*check.Mntent, 0, 3+len(libPaths))
 | 
								mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
 | 
				
			||||||
			mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
 | 
								mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
 | 
				
			||||||
			mnt = append(mnt, tc.mnt...)
 | 
								mnt = append(mnt, tc.mnt...)
 | 
				
			||||||
			mnt = append(mnt,
 | 
								mnt = append(mnt,
 | 
				
			||||||
				&check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
 | 
									e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
				&check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
 | 
									e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
 | 
				
			||||||
 | 
									e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			for _, name := range libPaths {
 | 
								for _, name := range libPaths {
 | 
				
			||||||
				mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1})
 | 
									mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"})
 | 
								mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
 | 
				
			||||||
			mntentWant := new(bytes.Buffer)
 | 
								want := new(bytes.Buffer)
 | 
				
			||||||
			if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
 | 
								if err := gob.NewEncoder(want).Encode(mnt); err != nil {
 | 
				
			||||||
				t.Fatalf("cannot serialise mntent: %v", err)
 | 
									t.Fatalf("cannot serialise expected mount points: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			container.Stdin = mntentWant
 | 
								container.Stdin = want
 | 
				
			||||||
 | 
					 | 
				
			||||||
			// needs /proc to check mntent
 | 
					 | 
				
			||||||
			container.Proc("/proc")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err := container.Start(); err != nil {
 | 
								if err := container.Start(); err != nil {
 | 
				
			||||||
				fmsg.PrintBaseError(err, "start:")
 | 
									fmsg.PrintBaseError(err, "start:")
 | 
				
			||||||
@ -134,6 +146,21 @@ 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) {
 | 
					func TestContainerString(t *testing.T) {
 | 
				
			||||||
	container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
 | 
						container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
 | 
				
			||||||
	container.Flags |= sandbox.FAllowDevel
 | 
						container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
@ -171,9 +198,55 @@ func TestHelperCheckContainer(t *testing.T) {
 | 
				
			|||||||
		} else if name != os.Args[5] {
 | 
							} else if name != os.Args[5] {
 | 
				
			||||||
			t.Errorf("Hostname: %q, want %q", 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 {
 | 
					func commandContext(ctx context.Context) *exec.Cmd {
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type initParams struct {
 | 
					type initParams struct {
 | 
				
			||||||
	InitParams
 | 
						Params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	HostUid, HostGid int
 | 
						HostUid, HostGid int
 | 
				
			||||||
	// extra files count
 | 
						// extra files count
 | 
				
			||||||
@ -98,6 +98,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oldmask := syscall.Umask(0)
 | 
				
			||||||
	if params.Hostname != "" {
 | 
						if params.Hostname != "" {
 | 
				
			||||||
		if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
 | 
							if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
 | 
				
			||||||
			log.Fatalf("cannot set hostname: %v", err)
 | 
								log.Fatalf("cannot set hostname: %v", err)
 | 
				
			||||||
@ -114,6 +115,19 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
		log.Fatalf("cannot make / rslave: %v", err)
 | 
							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",
 | 
						if err := syscall.Mount("rootfs", basePath, "tmpfs",
 | 
				
			||||||
		syscall.MS_NODEV|syscall.MS_NOSUID,
 | 
							syscall.MS_NODEV|syscall.MS_NOSUID,
 | 
				
			||||||
		""); err != nil {
 | 
							""); err != nil {
 | 
				
			||||||
@ -143,8 +157,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i, op := range *params.Ops {
 | 
						for i, op := range *params.Ops {
 | 
				
			||||||
		msg.Verbosef("mounting %s", op)
 | 
							// ops already checked during early setup
 | 
				
			||||||
		if err := op.apply(¶ms.InitParams); err != nil {
 | 
							msg.Verbosef("%s %s", op.prefix(), op)
 | 
				
			||||||
 | 
							if err := op.apply(¶ms.Params); err != nil {
 | 
				
			||||||
			msg.PrintBaseErr(err,
 | 
								msg.PrintBaseErr(err,
 | 
				
			||||||
				fmt.Sprintf("cannot apply op %d:", i))
 | 
									fmt.Sprintf("cannot apply op %d:", i))
 | 
				
			||||||
			msg.BeforeExit()
 | 
								msg.BeforeExit()
 | 
				
			||||||
@ -216,6 +231,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
	for i := range extraFiles {
 | 
						for i := range extraFiles {
 | 
				
			||||||
		extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
 | 
							extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						syscall.Umask(oldmask)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		prepare initial process
 | 
							prepare initial process
 | 
				
			||||||
@ -223,7 +239,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	cmd := exec.Command(params.Path)
 | 
						cmd := exec.Command(params.Path)
 | 
				
			||||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
						cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
					 | 
				
			||||||
	cmd.Args = params.Args
 | 
						cmd.Args = params.Args
 | 
				
			||||||
	cmd.Env = params.Env
 | 
						cmd.Env = params.Env
 | 
				
			||||||
	cmd.ExtraFiles = extraFiles
 | 
						cmd.ExtraFiles = extraFiles
 | 
				
			||||||
@ -308,10 +323,13 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			|||||||
				switch {
 | 
									switch {
 | 
				
			||||||
				case w.wstatus.Exited():
 | 
									case w.wstatus.Exited():
 | 
				
			||||||
					r = w.wstatus.ExitStatus()
 | 
										r = w.wstatus.ExitStatus()
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
 | 
				
			||||||
				case w.wstatus.Signaled():
 | 
									case w.wstatus.Signaled():
 | 
				
			||||||
					r = 128 + int(w.wstatus.Signal())
 | 
										r = 128 + int(w.wstatus.Signal())
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
					r = 255
 | 
										r = 255
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with status %#x", w.wstatus)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				go func() {
 | 
									go func() {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										160
									
								
								sandbox/mount.go
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								sandbox/mount.go
									
									
									
									
									
								
							@ -4,86 +4,105 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"path/filepath"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
 | 
				
			||||||
	BindOptional = 1 << iota
 | 
						if eq {
 | 
				
			||||||
	BindSource
 | 
							msg.Verbosef("resolved %q flags %#x", target, flags)
 | 
				
			||||||
	BindRecursive
 | 
					 | 
				
			||||||
	BindWritable
 | 
					 | 
				
			||||||
	BindDevices
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func bindMount(src, dest string, flags int) error {
 | 
					 | 
				
			||||||
	target := toSysroot(dest)
 | 
					 | 
				
			||||||
	var source string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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 {
 | 
					 | 
				
			||||||
			source = toHost(rp)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else if flags&BindOptional != 0 {
 | 
					 | 
				
			||||||
		return msg.WrapErr(syscall.EINVAL,
 | 
					 | 
				
			||||||
			"flag source excludes optional")
 | 
					 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		source = toHost(src)
 | 
							msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if fi, err := os.Stat(source); err != nil {
 | 
						if err := syscall.Mount(source, target, "",
 | 
				
			||||||
		return msg.WrapErr(err, err.Error())
 | 
							syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
 | 
				
			||||||
	} else if fi.IsDir() {
 | 
					 | 
				
			||||||
		if err = os.MkdirAll(target, 0755); err != nil {
 | 
					 | 
				
			||||||
			return wrapErrSuffix(err,
 | 
					 | 
				
			||||||
				fmt.Sprintf("cannot create directory %q:", dest))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} 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))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return wrapErrSuffix(err,
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
			fmt.Sprintf("cannot create %q:", dest))
 | 
								fmt.Sprintf("cannot mount %q on %q:", source, target))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
 | 
						var targetFinal string
 | 
				
			||||||
	if flags&BindRecursive != 0 {
 | 
						if v, err := filepath.EvalSymlinks(target); err != nil {
 | 
				
			||||||
		mf |= syscall.MS_REC
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
	}
 | 
						} else {
 | 
				
			||||||
	if flags&BindWritable == 0 {
 | 
							targetFinal = v
 | 
				
			||||||
		mf |= syscall.MS_RDONLY
 | 
							if targetFinal != target {
 | 
				
			||||||
	}
 | 
								msg.Verbosef("target resolves to %q", targetFinal)
 | 
				
			||||||
	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))
 | 
						// final target path according to the kernel through proc
 | 
				
			||||||
 | 
						var targetKFinal string
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							var destFd int
 | 
				
			||||||
 | 
							if err := IgnoringEINTR(func() (err error) {
 | 
				
			||||||
 | 
								destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot open %q:", targetFinal))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if v, err := os.Readlink(p.fd(destFd)); err != nil {
 | 
				
			||||||
 | 
								return 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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
 | 
				
			||||||
 | 
						return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
 | 
				
			||||||
 | 
							n, err := d.Unfold(targetKFinal)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, syscall.ESTALE) {
 | 
				
			||||||
 | 
									return msg.WrapErr(err,
 | 
				
			||||||
 | 
										fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot unfold mount hierarchy:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = remountWithFlags(n, mf); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if flags&syscall.MS_REC == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for cur := range n.Collective() {
 | 
				
			||||||
 | 
								err = remountWithFlags(cur, mf)
 | 
				
			||||||
 | 
								if err != nil && !errors.Is(err, syscall.EACCES) {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
 | 
				
			||||||
 | 
						kf, unmatched := n.Flags()
 | 
				
			||||||
 | 
						if len(unmatched) != 0 {
 | 
				
			||||||
 | 
							msg.Verbosef("unmatched vfs options: %q", unmatched)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if kf&mf != mf {
 | 
				
			||||||
 | 
							return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
 | 
				
			||||||
 | 
								syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
 | 
				
			||||||
 | 
								""),
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot remount %q:", n.Clean))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
					func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
				
			||||||
	target := toSysroot(name)
 | 
						target := toSysroot(name)
 | 
				
			||||||
	if err := os.MkdirAll(target, perm); err != nil {
 | 
						if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
 | 
				
			||||||
		return err
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	opt := fmt.Sprintf("mode=%#o", perm)
 | 
						opt := fmt.Sprintf("mode=%#o", perm)
 | 
				
			||||||
	if size > 0 {
 | 
						if size > 0 {
 | 
				
			||||||
@ -93,3 +112,14 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
				
			|||||||
		syscall.MS_NOSUID|syscall.MS_NODEV, opt),
 | 
							syscall.MS_NOSUID|syscall.MS_NODEV, opt),
 | 
				
			||||||
		fmt.Sprintf("cannot mount tmpfs on %q:", name))
 | 
							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,11 +2,15 @@ package sandbox
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@ -26,50 +30,65 @@ func toHost(name string) string {
 | 
				
			|||||||
	return path.Join(hostPath, name)
 | 
						return path.Join(hostPath, name)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func realpathHost(name string) (string, error) {
 | 
					func createFile(name string, perm, pperm os.FileMode, content []byte) error {
 | 
				
			||||||
	source := toHost(name)
 | 
						if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
 | 
				
			||||||
	rp, err := os.Readlink(source)
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	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)
 | 
						f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if content != nil {
 | 
						if content != nil {
 | 
				
			||||||
		_, err = f.Write(content)
 | 
							_, err = f.Write(content)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return errors.Join(f.Close(), err)
 | 
						return errors.Join(f.Close(), err)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ensureFile(name string, perm os.FileMode) error {
 | 
					func ensureFile(name string, perm, pperm os.FileMode) error {
 | 
				
			||||||
	fi, err := os.Stat(name)
 | 
						fi, err := os.Stat(name)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if !os.IsNotExist(err) {
 | 
							if !os.IsNotExist(err) {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return createFile(name, perm, nil)
 | 
							return createFile(name, perm, pperm, nil)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
 | 
						if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
 | 
				
			||||||
		err = syscall.EISDIR
 | 
							err = msg.WrapErr(syscall.EISDIR,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is a directory", name))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
						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)
 | 
									t.Errorf("Close: error = %v", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 {
 | 
								if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
 | 
				
			||||||
				t.Fatalf("Export() hash = %x, want %x",
 | 
									t.Fatalf("Export() hash = %x, want %x",
 | 
				
			||||||
					got, tc.want)
 | 
										got, tc.want)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -111,11 +111,14 @@ func TestExport(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	t.Run("close partial read", func(t *testing.T) {
 | 
						t.Run("close partial read", func(t *testing.T) {
 | 
				
			||||||
		e := seccomp.New(0)
 | 
							e := seccomp.New(0)
 | 
				
			||||||
		if _, err := e.Read(make([]byte, 0)); err != nil {
 | 
							if _, err := e.Read(nil); err != nil {
 | 
				
			||||||
			t.Errorf("Read: error = %v", err)
 | 
								t.Errorf("Read: error = %v", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) {
 | 
							// 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)) {
 | 
				
			||||||
			t.Errorf("Close: error = %v", err)
 | 
								t.Errorf("Close: error = %v", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,8 @@ import (
 | 
				
			|||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
	"unsafe"
 | 
						"unsafe"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -14,20 +16,77 @@ func init() { gob.Register(new(BindMount)) }
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// BindMount bind mounts host path Source on container path Target.
 | 
					// BindMount bind mounts host path Source on container path Target.
 | 
				
			||||||
type BindMount struct {
 | 
					type BindMount struct {
 | 
				
			||||||
	Source, Target string
 | 
						Source, SourceFinal, Target string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Flags int
 | 
						Flags int
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (b *BindMount) apply(*InitParams) error {
 | 
					const (
 | 
				
			||||||
	if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
 | 
						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) {
 | 
				
			||||||
		return msg.WrapErr(syscall.EBADE,
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
			"path is not absolute")
 | 
								"path is not absolute")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return bindMount(b.Source, b.Target, b.Flags)
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
 | 
					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 {
 | 
					func (b *BindMount) String() string {
 | 
				
			||||||
	if b.Source == b.Target {
 | 
						if b.Source == b.Target {
 | 
				
			||||||
		return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
 | 
							return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
 | 
				
			||||||
@ -35,54 +94,70 @@ func (b *BindMount) String() string {
 | 
				
			|||||||
	return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
 | 
						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 {
 | 
					func (f *Ops) Bind(source, target string, flags int) *Ops {
 | 
				
			||||||
	*f = append(*f, &BindMount{source, target, flags | BindRecursive})
 | 
						*f = append(*f, &BindMount{source, "", target, flags})
 | 
				
			||||||
	return f
 | 
						return f
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() { gob.Register(new(MountProc)) }
 | 
					func init() { gob.Register(new(MountProc)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MountProc mounts a private proc instance on container Path.
 | 
					// MountProc mounts a private instance of proc.
 | 
				
			||||||
type MountProc struct {
 | 
					type MountProc string
 | 
				
			||||||
	Path string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *MountProc) apply(*InitParams) error {
 | 
					func (p MountProc) early(*Params) error { return nil }
 | 
				
			||||||
	if !path.IsAbs(p.Path) {
 | 
					func (p MountProc) apply(*Params) error {
 | 
				
			||||||
 | 
						v := string(p)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !path.IsAbs(v) {
 | 
				
			||||||
		return msg.WrapErr(syscall.EBADE,
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
			fmt.Sprintf("path %q is not absolute", p.Path))
 | 
								fmt.Sprintf("path %q is not absolute", v))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	target := toSysroot(p.Path)
 | 
						target := toSysroot(v)
 | 
				
			||||||
	if err := os.MkdirAll(target, 0755); err != nil {
 | 
						if err := os.MkdirAll(target, 0755); err != nil {
 | 
				
			||||||
		return msg.WrapErr(err, err.Error())
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return wrapErrSuffix(syscall.Mount("proc", target, "proc",
 | 
						return wrapErrSuffix(syscall.Mount("proc", target, "proc",
 | 
				
			||||||
		syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
 | 
							syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
 | 
				
			||||||
		fmt.Sprintf("cannot mount proc on %q:", p.Path))
 | 
							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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() { gob.Register(new(MountDev)) }
 | 
					func init() { gob.Register(new(MountDev)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MountDev mounts dev on container Path.
 | 
					// MountDev mounts part of host dev.
 | 
				
			||||||
type MountDev struct {
 | 
					type MountDev string
 | 
				
			||||||
	Path string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *MountDev) apply(params *InitParams) error {
 | 
					func (d MountDev) early(*Params) error { return nil }
 | 
				
			||||||
	if !path.IsAbs(d.Path) {
 | 
					func (d MountDev) apply(params *Params) error {
 | 
				
			||||||
 | 
						v := string(d)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !path.IsAbs(v) {
 | 
				
			||||||
		return msg.WrapErr(syscall.EBADE,
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
			fmt.Sprintf("path %q is not absolute", d.Path))
 | 
								fmt.Sprintf("path %q is not absolute", v))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	target := toSysroot(d.Path)
 | 
						target := toSysroot(v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil {
 | 
						if err := mountTmpfs("devtmpfs", v, 0, 0755); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
 | 
						for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
 | 
				
			||||||
		if err := bindMount(
 | 
							targetPath := toSysroot(path.Join(v, name))
 | 
				
			||||||
			"/dev/"+name, path.Join(d.Path, name),
 | 
							if err := ensureFile(targetPath, 0444, 0755); err != nil {
 | 
				
			||||||
			BindSource|BindDevices,
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := hostProc.bindMount(
 | 
				
			||||||
 | 
								toHost("/dev/"+name),
 | 
				
			||||||
 | 
								targetPath,
 | 
				
			||||||
 | 
								0,
 | 
				
			||||||
 | 
								true,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -125,9 +200,17 @@ func (d *MountDev) apply(params *InitParams) error {
 | 
				
			|||||||
			syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
 | 
								syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
 | 
				
			||||||
			uintptr(unsafe.Pointer(&buf[0])),
 | 
								uintptr(unsafe.Pointer(&buf[0])),
 | 
				
			||||||
		); errno == 0 {
 | 
							); errno == 0 {
 | 
				
			||||||
			if err := bindMount(
 | 
								consolePath := toSysroot(path.Join(v, "console"))
 | 
				
			||||||
				"/proc/self/fd/1", path.Join(d.Path, "console"),
 | 
								if err := ensureFile(consolePath, 0444, 0755); err != nil {
 | 
				
			||||||
				BindDevices,
 | 
									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,
 | 
				
			||||||
			); err != nil {
 | 
								); err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -137,17 +220,42 @@ func (d *MountDev) apply(params *InitParams) error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *MountDev) Is(op Op) bool  { vd, ok := op.(*MountDev); return ok && *d == *vd }
 | 
					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 (MountDev) prefix() string   { return "mounting" }
 | 
				
			||||||
 | 
					func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
 | 
				
			||||||
func (f *Ops) Dev(dest string) *Ops {
 | 
					func (f *Ops) Dev(dest string) *Ops {
 | 
				
			||||||
	*f = append(*f, &MountDev{dest})
 | 
						*f = append(*f, MountDev(dest))
 | 
				
			||||||
	return f
 | 
						return f
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *MountProc) Is(op Op) bool  { vp, ok := op.(*MountProc); return ok && *p == *vp }
 | 
					func init() { gob.Register(new(MountMqueue)) }
 | 
				
			||||||
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
 | 
					
 | 
				
			||||||
func (f *Ops) Proc(dest string) *Ops {
 | 
					// MountMqueue mounts a private mqueue instance on container Path.
 | 
				
			||||||
	*f = append(*f, &MountProc{dest})
 | 
					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))
 | 
				
			||||||
	return f
 | 
						return f
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -160,7 +268,8 @@ type MountTmpfs struct {
 | 
				
			|||||||
	Perm os.FileMode
 | 
						Perm os.FileMode
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (t *MountTmpfs) apply(*InitParams) error {
 | 
					func (t *MountTmpfs) early(*Params) error { return nil }
 | 
				
			||||||
 | 
					func (t *MountTmpfs) apply(*Params) error {
 | 
				
			||||||
	if !path.IsAbs(t.Path) {
 | 
						if !path.IsAbs(t.Path) {
 | 
				
			||||||
		return msg.WrapErr(syscall.EBADE,
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
			fmt.Sprintf("path %q is not absolute", t.Path))
 | 
								fmt.Sprintf("path %q is not absolute", t.Path))
 | 
				
			||||||
@ -173,8 +282,133 @@ func (t *MountTmpfs) apply(*InitParams) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (t *MountTmpfs) Is(op Op) bool  { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
 | 
					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 (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 {
 | 
					func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
 | 
				
			||||||
	*f = append(*f, &MountTmpfs{dest, size, perm})
 | 
						*f = append(*f, &MountTmpfs{dest, size, perm})
 | 
				
			||||||
	return f
 | 
						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,6 +2,12 @@ package sandbox
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import "syscall"
 | 
					import "syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						O_PATH              = 0x200000
 | 
				
			||||||
 | 
						PR_SET_NO_NEW_PRIVS = 0x26
 | 
				
			||||||
 | 
						CAP_SYS_ADMIN       = 0x15
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	SUID_DUMP_DISABLE = iota
 | 
						SUID_DUMP_DISABLE = iota
 | 
				
			||||||
	SUID_DUMP_USER
 | 
						SUID_DUMP_USER
 | 
				
			||||||
@ -16,14 +22,6 @@ func SetDumpable(dumpable uintptr) error {
 | 
				
			|||||||
	return nil
 | 
						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
 | 
					// IgnoringEINTR makes a function call and repeats it if it returns an
 | 
				
			||||||
// EINTR error. This appears to be required even though we install all
 | 
					// EINTR error. This appears to be required even though we install all
 | 
				
			||||||
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
 | 
					// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								sandbox/vfs/mangle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								sandbox/vfs/mangle.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					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])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								sandbox/vfs/mangle_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								sandbox/vfs/mangle_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										260
									
								
								sandbox/vfs/mountinfo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								sandbox/vfs/mountinfo.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,260 @@
 | 
				
			|||||||
 | 
					// 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										404
									
								
								sandbox/vfs/mountinfo_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								sandbox/vfs/mountinfo_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,404 @@
 | 
				
			|||||||
 | 
					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`
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										107
									
								
								sandbox/vfs/unfold.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								sandbox/vfs/unfold.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										93
									
								
								sandbox/vfs/unfold_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								sandbox/vfs/unfold_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					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,6 +4,12 @@
 | 
				
			|||||||
  config,
 | 
					  config,
 | 
				
			||||||
  ...
 | 
					  ...
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
 | 
					let
 | 
				
			||||||
 | 
					  testCases = import ./sandbox/case {
 | 
				
			||||||
 | 
					    inherit (pkgs) lib callPackage foot;
 | 
				
			||||||
 | 
					    inherit (config.environment.fortify.package) version;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					in
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  users.users = {
 | 
					  users.users = {
 | 
				
			||||||
    alice = {
 | 
					    alice = {
 | 
				
			||||||
@ -102,21 +108,10 @@
 | 
				
			|||||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
					    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apps = [
 | 
					    apps = [
 | 
				
			||||||
      {
 | 
					      testCases.preset
 | 
				
			||||||
        name = "check-sandbox";
 | 
					      testCases.tty
 | 
				
			||||||
        verbose = true;
 | 
					      testCases.mapuid
 | 
				
			||||||
        share = pkgs.foot;
 | 
					
 | 
				
			||||||
        packages = [ ];
 | 
					 | 
				
			||||||
        command = "${pkgs.callPackage ./sandbox {
 | 
					 | 
				
			||||||
          inherit (config.environment.fortify.package) version;
 | 
					 | 
				
			||||||
        }}";
 | 
					 | 
				
			||||||
        extraPaths = [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            src = "/proc/mounts";
 | 
					 | 
				
			||||||
            dst = "/.fortify/host-mounts";
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name = "ne-foot";
 | 
					        name = "ne-foot";
 | 
				
			||||||
        verbose = true;
 | 
					        verbose = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
  nixosTest,
 | 
					  nixosTest,
 | 
				
			||||||
 | 
					  buildFHSEnv,
 | 
				
			||||||
  writeShellScriptBin,
 | 
					  writeShellScriptBin,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  system,
 | 
					  system,
 | 
				
			||||||
@ -12,6 +13,21 @@ nixosTest {
 | 
				
			|||||||
  name = "fortify" + (if withRace then "-race" else "");
 | 
					  name = "fortify" + (if withRace then "-race" else "");
 | 
				
			||||||
  nodes.machine =
 | 
					  nodes.machine =
 | 
				
			||||||
    { options, pkgs, ... }:
 | 
					    { 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 = [
 | 
					      environment.systemPackages = [
 | 
				
			||||||
        # For go tests:
 | 
					        # For go tests:
 | 
				
			||||||
@ -21,7 +37,7 @@ nixosTest {
 | 
				
			|||||||
          cp -r "${self.packages.${system}.fortify.src}" "$WORK"
 | 
					          cp -r "${self.packages.${system}.fortify.src}" "$WORK"
 | 
				
			||||||
          chmod -R +w "$WORK"
 | 
					          chmod -R +w "$WORK"
 | 
				
			||||||
          cd "$WORK"
 | 
					          cd "$WORK"
 | 
				
			||||||
          ${self.packages.${system}.fhs}/bin/fortify-fhs -c \
 | 
					          ${fhs}/bin/fortify-fhs -c \
 | 
				
			||||||
            'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
 | 
					            'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
 | 
				
			||||||
        '')
 | 
					        '')
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,9 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					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
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
@ -16,76 +22,89 @@ var (
 | 
				
			|||||||
func printf(format string, v ...any) { printfFunc(format, v...) }
 | 
					func printf(format string, v ...any) { printfFunc(format, v...) }
 | 
				
			||||||
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
 | 
					func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mustDecode(wantFile string, v any) {
 | 
					type TestCase struct {
 | 
				
			||||||
	if f, err := os.Open(wantFile); err != nil {
 | 
						FS      *FS               `json:"fs"`
 | 
				
			||||||
		fatalf("cannot open %q: %v", wantFile, err)
 | 
						Mount   []*MountinfoEntry `json:"mount"`
 | 
				
			||||||
	} else if err = json.NewDecoder(f).Decode(v); err != nil {
 | 
						Seccomp bool              `json:"seccomp"`
 | 
				
			||||||
		fatalf("cannot decode %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
	} else if err = f.Close(); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot close %q: %v", wantFile, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustAssertMounts(name, hostMountsFile, wantFile string) {
 | 
					type T struct {
 | 
				
			||||||
	hostMounts := make([]*Mntent, 0, 128)
 | 
						FS fs.FS
 | 
				
			||||||
	if err := IterMounts(hostMountsFile, func(e *Mntent) {
 | 
					
 | 
				
			||||||
		hostMounts = append(hostMounts, e)
 | 
						MountsPath string
 | 
				
			||||||
	}); err != nil {
 | 
					}
 | 
				
			||||||
		fatalf("cannot parse host mounts: %v", err)
 | 
					
 | 
				
			||||||
 | 
					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")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var want []Mntent
 | 
						if want.Mount != nil {
 | 
				
			||||||
	mustDecode(wantFile, &want)
 | 
							var fail bool
 | 
				
			||||||
 | 
							m := mustParseMountinfo(t.MountsPath)
 | 
				
			||||||
	for i := range want {
 | 
							i := 0
 | 
				
			||||||
		if want[i].Opts == "host_passthrough" {
 | 
							for ent := range m.Entries() {
 | 
				
			||||||
			for _, ent := range hostMounts {
 | 
								if i == len(want.Mount) {
 | 
				
			||||||
				if want[i].FSName == ent.FSName {
 | 
									fatalf("got more than %d entries", i)
 | 
				
			||||||
					// 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)
 | 
								if !ent.EqualWithIgnore(want.Mount[i], "//ignore") {
 | 
				
			||||||
		out:
 | 
									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")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	i := 0
 | 
						if want.Seccomp {
 | 
				
			||||||
	if err := IterMounts(name, func(e *Mntent) {
 | 
							if TrySyscalls() != nil {
 | 
				
			||||||
		if i == len(want) {
 | 
								os.Exit(1)
 | 
				
			||||||
			fatalf("got more than %d entries", i)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if !e.Is(&want[i]) {
 | 
						} else {
 | 
				
			||||||
			fatalf("entry %d\n got: %s\nwant: %s", i,
 | 
							printf("[SKIP] skipping seccomp check")
 | 
				
			||||||
				e, &want[i])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		printf("%s", e)
 | 
					 | 
				
			||||||
		i++
 | 
					 | 
				
			||||||
	}); err != nil {
 | 
					 | 
				
			||||||
		fatalf("cannot iterate mounts: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustAssertFS(e fs.FS, wantFile string) {
 | 
					func mustDecode(wantFilePath string, v any) {
 | 
				
			||||||
	var want *FS
 | 
						if f, err := os.Open(wantFilePath); err != nil {
 | 
				
			||||||
	mustDecode(wantFile, &want)
 | 
							fatalf("cannot open %q: %v", wantFilePath, err)
 | 
				
			||||||
	if want == nil {
 | 
						} else if err = json.NewDecoder(f).Decode(v); err != nil {
 | 
				
			||||||
		fatalf("invalid payload")
 | 
							fatalf("cannot decode %q: %v", wantFilePath, err)
 | 
				
			||||||
 | 
						} else if err = f.Close(); err != nil {
 | 
				
			||||||
 | 
							fatalf("cannot close %q: %v", wantFilePath, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := want.Compare(".", e); err != nil {
 | 
					func mustParseMountinfo(name string) *Mountinfo {
 | 
				
			||||||
 | 
						m := NewMountinfo(name)
 | 
				
			||||||
 | 
						if err := m.Parse(); err != nil {
 | 
				
			||||||
		fatalf("%v", err)
 | 
							fatalf("%v", err)
 | 
				
			||||||
 | 
							panic("unreachable")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
						return m
 | 
				
			||||||
 | 
					 | 
				
			||||||
func MustAssertSeccomp() {
 | 
					 | 
				
			||||||
	if TrySyscalls() != nil {
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								test/sandbox/assert.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								test/sandbox/assert.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					  '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								test/sandbox/case/default.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								test/sandbox/case/default.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										221
									
								
								test/sandbox/case/mapuid.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								test/sandbox/case/mapuid.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,221 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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,29 +1,17 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  fs,
 | 
				
			||||||
  writeText,
 | 
					  ent,
 | 
				
			||||||
  buildGoModule,
 | 
					  ignore,
 | 
				
			||||||
 | 
					 | 
				
			||||||
  version,
 | 
					 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
let
 | 
					{
 | 
				
			||||||
  wantFS =
 | 
					  name = "preset";
 | 
				
			||||||
    let
 | 
					  tty = false;
 | 
				
			||||||
      fs = mode: dir: data: {
 | 
					  mapRealUid = false;
 | 
				
			||||||
        mode = lib.fromHexString mode;
 | 
					
 | 
				
			||||||
        inherit
 | 
					  want = {
 | 
				
			||||||
          dir
 | 
					    fs = fs "dead" {
 | 
				
			||||||
          data
 | 
					 | 
				
			||||||
          ;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    in
 | 
					 | 
				
			||||||
    fs "dead" {
 | 
					 | 
				
			||||||
      ".fortify" = fs "800001ed" {
 | 
					      ".fortify" = fs "800001ed" {
 | 
				
			||||||
        etc = fs "800001ed" null null;
 | 
					        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;
 | 
					      } null;
 | 
				
			||||||
      bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
 | 
					      bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
 | 
				
			||||||
      dev = fs "800001ed" {
 | 
					      dev = fs "800001ed" {
 | 
				
			||||||
@ -191,24 +179,43 @@ let
 | 
				
			|||||||
      } null;
 | 
					      } null;
 | 
				
			||||||
    } null;
 | 
					    } null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mainFile = writeText "main.go" ''
 | 
					    mount = [
 | 
				
			||||||
    package main
 | 
					      (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")
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import "os"
 | 
					    seccomp = true;
 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										223
									
								
								test/sandbox/case/tty.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								test/sandbox/case/tty.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,223 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  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,
 | 
					  writeShellScript,
 | 
				
			||||||
 | 
					  writeText,
 | 
				
			||||||
  callPackage,
 | 
					  callPackage,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  version,
 | 
					  version,
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
writeShellScript "check-sandbox" ''
 | 
					name: want:
 | 
				
			||||||
 | 
					writeShellScript "fortify-${name}-check-sandbox-script" ''
 | 
				
			||||||
  set -e
 | 
					  set -e
 | 
				
			||||||
  ${callPackage ./mount.nix { inherit version; }}/bin/test
 | 
					  ${callPackage ./assert.nix { inherit version; }}/bin/test \
 | 
				
			||||||
  ${callPackage ./fs.nix { inherit version; }}/bin/test
 | 
					    ${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
 | 
				
			||||||
  ${callPackage ./seccomp.nix { inherit version; }}/bin/test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  touch /tmp/sandbox-ok
 | 
					  touch /tmp/sandbox-ok
 | 
				
			||||||
''
 | 
					''
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,7 @@ func printDir(prefix string, dir []fs.DirEntry) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		names[i] = fmt.Sprintf("%q", name)
 | 
							names[i] = fmt.Sprintf("%q", name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	printf("[FAIL] d %q: %s", prefix, strings.Join(names, " "))
 | 
						printf("[FAIL] d %s: %s", prefix, strings.Join(names, " "))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *FS) Compare(prefix string, e fs.FS) error {
 | 
					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 {
 | 
								if fi, err := got.Info(); err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			} else if fi.Mode() != want.Mode {
 | 
								} else if fi.Mode() != want.Mode {
 | 
				
			||||||
				printf("[FAIL] m %q: %x, want %x",
 | 
									printf("[FAIL] m %s: %x, want %x",
 | 
				
			||||||
					name, uint32(fi.Mode()), uint32(want.Mode))
 | 
										name, uint32(fi.Mode()), uint32(want.Mode))
 | 
				
			||||||
				return ErrFSBadMode
 | 
									return ErrFSBadMode
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -84,6 +84,8 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
 | 
				
			|||||||
					return err
 | 
										return err
 | 
				
			||||||
				} else if string(v) != *want.Data {
 | 
									} else if string(v) != *want.Data {
 | 
				
			||||||
					printf("[FAIL] f %s", name)
 | 
										printf("[FAIL] f %s", name)
 | 
				
			||||||
 | 
										printf("got:  %s", v)
 | 
				
			||||||
 | 
										printf("want: %s", *want.Data)
 | 
				
			||||||
					return ErrFSBadData
 | 
										return ErrFSBadData
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				printf("[ OK ] f %s", name)
 | 
									printf("[ OK ] f %s", name)
 | 
				
			||||||
 | 
				
			|||||||
@ -31,16 +31,16 @@ func TestCompare(t *testing.T) {
 | 
				
			|||||||
			"[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
 | 
								"[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
 | 
				
			||||||
		{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
 | 
							{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
 | 
				
			||||||
			&sandbox.FS{Dir: make(map[string]*sandbox.FS)},
 | 
								&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}},
 | 
							{"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
 | 
				
			||||||
			&sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}},
 | 
								&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}},
 | 
							{"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)}}},
 | 
								&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}},
 | 
							{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
 | 
				
			||||||
			&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
 | 
								&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},
 | 
							{"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
 | 
				
			||||||
			"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
 | 
								"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
 | 
				
			||||||
			"etc/group":  {Data: []byte(fsGroupSample), 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{
 | 
							}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
 | 
				
			||||||
			"passwd": {Mode: 0x1a4, Data: &fsGroupSample},
 | 
								"passwd": {Mode: 0x1a4, Data: &fsGroupSample},
 | 
				
			||||||
			"group":  {Mode: 0x1a4, Data: &fsGroupSample},
 | 
								"group":  {Mode: 0x1a4, Data: &fsGroupSample},
 | 
				
			||||||
		}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00", sandbox.ErrFSBadData},
 | 
							}}}}, "[ 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},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
@ -75,10 +75,4 @@ 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,146 +1,157 @@
 | 
				
			|||||||
package sandbox
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 | 
					#cgo linux pkg-config: --static mount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <stdlib.h>
 | 
					#include <stdlib.h>
 | 
				
			||||||
#include <stdio.h>
 | 
					#include <stdio.h>
 | 
				
			||||||
#include <mntent.h>
 | 
					#include <libmount.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const char *F_PROC_MOUNTS = "";
 | 
					const char *F_MOUNTINFO_PATH = "/proc/self/mountinfo";
 | 
				
			||||||
const char *F_SET_TYPE = "r";
 | 
					 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
import "C"
 | 
					import "C"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"iter"
 | 
				
			||||||
	"runtime"
 | 
						"runtime"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"unsafe"
 | 
						"unsafe"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Mntent struct {
 | 
					var (
 | 
				
			||||||
	/* name of mounted filesystem */
 | 
						ErrMountinfoParse = errors.New("invalid mountinfo records")
 | 
				
			||||||
	FSName string `json:"fsname"`
 | 
						ErrMountinfoIter  = errors.New("cannot allocate iterator")
 | 
				
			||||||
	/* filesystem path prefix */
 | 
						ErrMountinfoFault = errors.New("cannot iterate on filesystems")
 | 
				
			||||||
	Dir string `json:"dir"`
 | 
					)
 | 
				
			||||||
	/* mount type (see mntent.h) */
 | 
					 | 
				
			||||||
	Type string `json:"type"`
 | 
					 | 
				
			||||||
	/* mount options (see mntent.h) */
 | 
					 | 
				
			||||||
	Opts string `json:"opts"`
 | 
					 | 
				
			||||||
	/* dump frequency in days */
 | 
					 | 
				
			||||||
	Freq int `json:"freq"`
 | 
					 | 
				
			||||||
	/* pass number on parallel fsck */
 | 
					 | 
				
			||||||
	Passno int `json:"passno"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (e *Mntent) String() string {
 | 
					type (
 | 
				
			||||||
	return fmt.Sprintf("%s %s %s %s %d %d",
 | 
						Mountinfo struct {
 | 
				
			||||||
		e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
 | 
							mu  sync.RWMutex
 | 
				
			||||||
}
 | 
							p   string
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (e *Mntent) Is(want *Mntent) bool {
 | 
							tb  *C.struct_libmnt_table
 | 
				
			||||||
	if want == nil {
 | 
							itr *C.struct_libmnt_iter
 | 
				
			||||||
		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 IterMounts(name string, f func(e *Mntent)) error {
 | 
							fs *C.struct_libmnt_fs
 | 
				
			||||||
	m := new(mounts)
 | 
					 | 
				
			||||||
	m.p = name
 | 
					 | 
				
			||||||
	if err := m.open(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for m.scan() {
 | 
						// MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry.
 | 
				
			||||||
		e := new(Mntent)
 | 
						MountinfoEntry struct {
 | 
				
			||||||
		m.copy(e)
 | 
							// mount ID: a unique ID for the mount (may be reused after umount(2)).
 | 
				
			||||||
		f(e)
 | 
							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"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m.close()
 | 
					func (m *Mountinfo) copy(v *MountinfoEntry) {
 | 
				
			||||||
	return m.Err()
 | 
						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 mounts struct {
 | 
					func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
 | 
				
			||||||
	p  string
 | 
					 | 
				
			||||||
	f  *C.FILE
 | 
					 | 
				
			||||||
	mu sync.RWMutex
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ent *C.struct_mntent
 | 
					func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
 | 
				
			||||||
	err error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *mounts) open() error {
 | 
					func (m *Mountinfo) Parse() error {
 | 
				
			||||||
	m.mu.Lock()
 | 
						m.mu.Lock()
 | 
				
			||||||
	defer m.mu.Unlock()
 | 
						defer m.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if m.f != nil {
 | 
						if m.tb != nil {
 | 
				
			||||||
		panic("open called twice")
 | 
							panic("open called twice")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if m.p == "" {
 | 
						if m.p == "" {
 | 
				
			||||||
		m.p = "/proc/mounts"
 | 
							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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	name := C.CString(m.p)
 | 
						runtime.SetFinalizer(m, (*Mountinfo).Unref)
 | 
				
			||||||
	f, err := C.setmntent(name, C.F_SET_TYPE)
 | 
						return nil
 | 
				
			||||||
	C.free(unsafe.Pointer(name))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if f == nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	m.f = f
 | 
					 | 
				
			||||||
	runtime.SetFinalizer(m, (*mounts).close)
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *mounts) close() {
 | 
					func (m *Mountinfo) Unref() {
 | 
				
			||||||
	m.mu.Lock()
 | 
						m.mu.Lock()
 | 
				
			||||||
	defer m.mu.Unlock()
 | 
						defer m.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if m.f == nil {
 | 
						if m.tb == nil {
 | 
				
			||||||
		panic("close called before open")
 | 
							panic("unref called before parse")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	C.endmntent(m.f)
 | 
						C.mnt_unref_table(m.tb)
 | 
				
			||||||
 | 
						C.mnt_free_iter(m.itr)
 | 
				
			||||||
	runtime.SetFinalizer(m, nil)
 | 
						runtime.SetFinalizer(m, nil)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *mounts) scan() bool {
 | 
					func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] {
 | 
				
			||||||
	m.mu.Lock()
 | 
						return func(yield func(*MountinfoEntry) bool) {
 | 
				
			||||||
	defer m.mu.Unlock()
 | 
							m.mu.Lock()
 | 
				
			||||||
 | 
							defer m.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if m.f == nil {
 | 
							C.mnt_reset_iter(m.itr, -1)
 | 
				
			||||||
		panic("invalid file")
 | 
					
 | 
				
			||||||
 | 
							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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	m.ent, m.err = C.getmntent(m.f)
 | 
					 | 
				
			||||||
	return m.ent != nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *mounts) Err() error {
 | 
					func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool {
 | 
				
			||||||
	m.mu.RLock()
 | 
						return (e.ID == want.ID || want.ID == -1) &&
 | 
				
			||||||
	defer m.mu.RUnlock()
 | 
							(e.Parent == want.Parent || want.Parent == -1) &&
 | 
				
			||||||
 | 
							(e.Root == want.Root || want.Root == ignore) &&
 | 
				
			||||||
	return m.err
 | 
							(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) copy(v *Mntent) {
 | 
					func (e *MountinfoEntry) String() string {
 | 
				
			||||||
	m.mu.RLock()
 | 
						return fmt.Sprintf("%d %d %s %s %s %s %s %s",
 | 
				
			||||||
	defer m.mu.RUnlock()
 | 
							e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.ent == nil {
 | 
					 | 
				
			||||||
		panic("invalid entry")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	v.FSName = C.GoString(m.ent.mnt_fsname)
 | 
					 | 
				
			||||||
	v.Dir = C.GoString(m.ent.mnt_dir)
 | 
					 | 
				
			||||||
	v.Type = C.GoString(m.ent.mnt_type)
 | 
					 | 
				
			||||||
	v.Opts = C.GoString(m.ent.mnt_opts)
 | 
					 | 
				
			||||||
	v.Freq = int(m.ent.mnt_freq)
 | 
					 | 
				
			||||||
	v.Passno = int(m.ent.mnt_passno)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,79 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  writeText,
 | 
					 | 
				
			||||||
  buildGoModule,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  version,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
let
 | 
					 | 
				
			||||||
  wantMounts =
 | 
					 | 
				
			||||||
    let
 | 
					 | 
				
			||||||
      ent = fsname: dir: type: opts: freq: passno: {
 | 
					 | 
				
			||||||
        inherit
 | 
					 | 
				
			||||||
          fsname
 | 
					 | 
				
			||||||
          dir
 | 
					 | 
				
			||||||
          type
 | 
					 | 
				
			||||||
          opts
 | 
					 | 
				
			||||||
          freq
 | 
					 | 
				
			||||||
          passno
 | 
					 | 
				
			||||||
          ;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    in
 | 
					 | 
				
			||||||
    [
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/" "tmpfs" "rw,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "proc" "/proc" "proc" "rw,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/.fortify" "tmpfs" "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/dev" "tmpfs" "rw,nosuid,nodev,relatime,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/null" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/zero" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/full" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/random" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/urandom" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/tty" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "devpts" "/dev/pts" "devpts" "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666" 0 0)
 | 
					 | 
				
			||||||
      (ent "mqueue" "/dev/mqueue" "mqueue" "rw,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/usr/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/nix/store" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/run/current-system" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/block" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/bus" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/class" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/dev" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "sysfs" "/sys/devices" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "overlay" "/run/opengl-driver" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
 | 
					 | 
				
			||||||
      (ent "devtmpfs" "/dev/dri" "devtmpfs" "host_passthrough" 0 0)
 | 
					 | 
				
			||||||
      (ent "proc" "/.fortify/host-mounts" "proc" "ro,nosuid,nodev,noexec,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/.fortify/etc" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user" "tmpfs" "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user/65534" "tmpfs" "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/tmp" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/var/lib/fortify/u0/a1" "ext4" "rw,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
 | 
					 | 
				
			||||||
      (ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
 | 
					 | 
				
			||||||
      (ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "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,80 +8,79 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/test/sandbox"
 | 
						"git.gensokyo.uk/security/fortify/test/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMounts(t *testing.T) {
 | 
					func TestMountinfo(t *testing.T) {
 | 
				
			||||||
	testCases := []struct {
 | 
						testCases := []struct {
 | 
				
			||||||
		name string
 | 
							name string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		sample string
 | 
							sample string
 | 
				
			||||||
		want   []sandbox.Mntent
 | 
							want   []*sandbox.MountinfoEntry
 | 
				
			||||||
	}{
 | 
						}{
 | 
				
			||||||
		{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
							{"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw
 | 
				
			||||||
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
 | 
					16 20 0:15 / /sys rw,relatime - sysfs /sys rw
 | 
				
			||||||
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
 | 
				
			||||||
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
 | 
					18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
 | 
				
			||||||
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
 | 
				
			||||||
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
 | 
				
			||||||
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
 | 
				
			||||||
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					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
 | 
				
			||||||
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
 | 
				
			||||||
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
 | 
				
			||||||
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
 | 
					25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
 | 
				
			||||||
mqueue /dev/mqueue mqueue rw,relatime 0 0
 | 
					26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
 | 
				
			||||||
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
 | 
					27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
 | 
					28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
 | 
				
			||||||
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
 | 
					29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
 | 
				
			||||||
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
 | 
				
			||||||
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
 | 
				
			||||||
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
				
			||||||
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
				
			||||||
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
 | 
					34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
 | 
					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
 | 
				
			||||||
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
 | 
					36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
 | 
				
			||||||
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
 | 
					37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
 | 
				
			||||||
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
 | 
				
			||||||
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
 | 
					39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
 | 
				
			||||||
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
 | 
					40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
 | 
				
			||||||
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
 | 
					41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
 | 
				
			||||||
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
					42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
 | 
				
			||||||
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
 | 
					43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
 | 
				
			||||||
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
 | 
					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
 | 
				
			||||||
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
 | 
					45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
 | 
				
			||||||
/dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0
 | 
					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
 | 
				
			||||||
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
 | 
					49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{
 | 
				
			||||||
`, []sandbox.Mntent{
 | 
								e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
 | 
				
			||||||
			{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
								e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
 | 
				
			||||||
			{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
 | 
				
			||||||
			{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
								e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
 | 
				
			||||||
			{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
								e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
 | 
				
			||||||
			{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
 | 
				
			||||||
			{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
 | 
				
			||||||
			{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
 | 
				
			||||||
			{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
 | 
				
			||||||
			{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
 | 
				
			||||||
			{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
 | 
				
			||||||
			{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
 | 
								e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
 | 
				
			||||||
			{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
 | 
								e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
 | 
				
			||||||
			{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
 | 
				
			||||||
			{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
				
			||||||
			{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
				
			||||||
			{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
				
			||||||
			{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
 | 
								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"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
 | 
				
			||||||
			{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
 | 
								e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
 | 
				
			||||||
			{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
								e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
 | 
				
			||||||
			{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
 | 
								e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
								e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
 | 
								e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
 | 
				
			||||||
			{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
								e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
 | 
				
			||||||
			{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
 | 
								e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
 | 
				
			||||||
			{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
 | 
								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"),
 | 
				
			||||||
			{"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
 | 
								e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
 | 
				
			||||||
			{"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},
 | 
					 | 
				
			||||||
		}},
 | 
							}},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,27 +91,33 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			i := 0
 | 
								m := sandbox.NewMountinfo(name)
 | 
				
			||||||
			if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
 | 
								if err := m.Parse(); err != nil {
 | 
				
			||||||
				if i == len(tc.want) {
 | 
									t.Fatalf("Parse: error = %v", err)
 | 
				
			||||||
					t.Errorf("IterMounts: got more than %d entries", i)
 | 
					 | 
				
			||||||
					t.FailNow()
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if *e != tc.want[i] {
 | 
					 | 
				
			||||||
					t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
 | 
					 | 
				
			||||||
						e, &tc.want[i])
 | 
					 | 
				
			||||||
					t.FailNow()
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				i++
 | 
					 | 
				
			||||||
			}); err != nil {
 | 
					 | 
				
			||||||
				t.Fatalf("IterMounts: error = %v", err)
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run(tc.name+" assert", func(t *testing.T) {
 | 
								i := 0
 | 
				
			||||||
			oldFatal := sandbox.SwapFatal(t.Fatalf)
 | 
								for ent := range m.Entries() {
 | 
				
			||||||
			t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
 | 
									if i == len(tc.want) {
 | 
				
			||||||
			sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want))
 | 
										t.Errorf("Entries: 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])
 | 
				
			||||||
 | 
										t.FailNow()
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										t.Logf("%s", ent)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									i++
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := m.Err(); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Mountinfo: error = %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m.Unref()
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := os.Remove(name); err != nil {
 | 
							if err := os.Remove(name); err != nil {
 | 
				
			||||||
@ -120,3 +125,18 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  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,9 +62,12 @@ def check_state(name, enablements):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    config = instance['config']
 | 
					    config = instance['config']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not (
 | 
					    command = f"{name}-start"
 | 
				
			||||||
            config['command'][0].endswith(f"{name}-start")):
 | 
					    if not (config['path'].startswith("/nix/store/")) or not (config['path'].endswith(command)):
 | 
				
			||||||
        raise Exception(f"unexpected command {instance['config']['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 config['confinement']['enablements'] != enablements:
 | 
					    if config['confinement']['enablements'] != enablements:
 | 
				
			||||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
					        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
				
			||||||
@ -102,9 +105,26 @@ 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":
 | 
					if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
				
			||||||
    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
					    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check sandbox state:
 | 
					# Check sandbox outcome:
 | 
				
			||||||
swaymsg("exec check-sandbox")
 | 
					check_offset = 0
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15)
 | 
					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}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Start fortify permissive defaults outside Wayland session:
 | 
					# Start fortify permissive defaults outside Wayland session:
 | 
				
			||||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
					print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
				
			||||||
@ -146,23 +166,23 @@ machine.succeed("pkill -9 mako")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with Wayland enablement:
 | 
					# Start app (foot) with Wayland enablement:
 | 
				
			||||||
swaymsg("exec ne-foot")
 | 
					swaymsg("exec ne-foot")
 | 
				
			||||||
wait_for_window("u0_a2@machine")
 | 
					wait_for_window(f"u0_a{aid(0)}@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10)
 | 
					machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=10)
 | 
				
			||||||
collect_state_ui("foot_wayland")
 | 
					collect_state_ui("foot_wayland")
 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					check_state("ne-foot", 1)
 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					# Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
 | 
					print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}"))
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
machine.wait_until_fails("pgrep foot", timeout=5)
 | 
					machine.wait_until_fails("pgrep foot", timeout=5)
 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002", timeout=5)
 | 
					machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with Wayland enablement from a terminal:
 | 
					# 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'")
 | 
					swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
				
			||||||
wait_for_window("u0_a2@machine")
 | 
					wait_for_window(f"u0_a{aid(0)}@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10)
 | 
					machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=10)
 | 
				
			||||||
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
 | 
					machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
 | 
				
			||||||
collect_state_ui("foot_wayland_term")
 | 
					collect_state_ui("foot_wayland_term")
 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					check_state("ne-foot", 1)
 | 
				
			||||||
@ -173,9 +193,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Test PulseAudio (fortify does not support PipeWire yet):
 | 
					# Test PulseAudio (fortify does not support PipeWire yet):
 | 
				
			||||||
swaymsg("exec pa-foot")
 | 
					swaymsg("exec pa-foot")
 | 
				
			||||||
wait_for_window("u0_a3@machine")
 | 
					wait_for_window(f"u0_a{aid(1)}@machine")
 | 
				
			||||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
					machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10)
 | 
					machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15)
 | 
				
			||||||
collect_state_ui("pulse_wayland")
 | 
					collect_state_ui("pulse_wayland")
 | 
				
			||||||
check_state("pa-foot", 9)
 | 
					check_state("pa-foot", 9)
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
@ -183,9 +203,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Test XWayland (foot does not support X):
 | 
					# Test XWayland (foot does not support X):
 | 
				
			||||||
swaymsg("exec x11-alacritty")
 | 
					swaymsg("exec x11-alacritty")
 | 
				
			||||||
wait_for_window("u0_a4@machine")
 | 
					wait_for_window(f"u0_a{aid(2)}@machine")
 | 
				
			||||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
					machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10)
 | 
					machine.wait_for_file(tmpdir_path(2, "x11-ok"), timeout=10)
 | 
				
			||||||
collect_state_ui("alacritty_x11")
 | 
					collect_state_ui("alacritty_x11")
 | 
				
			||||||
check_state("x11-alacritty", 2)
 | 
					check_state("x11-alacritty", 2)
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
@ -193,17 +213,17 @@ machine.wait_until_fails("pgrep alacritty", timeout=5)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Start app (foot) with direct Wayland access:
 | 
					# Start app (foot) with direct Wayland access:
 | 
				
			||||||
swaymsg("exec da-foot")
 | 
					swaymsg("exec da-foot")
 | 
				
			||||||
wait_for_window("u0_a5@machine")
 | 
					wait_for_window(f"u0_a{aid(3)}@machine")
 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
					machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n")
 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
 | 
					 | 
				
			||||||
collect_state_ui("foot_direct")
 | 
					collect_state_ui("foot_direct")
 | 
				
			||||||
 | 
					machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=10)
 | 
				
			||||||
check_state("da-foot", 1)
 | 
					check_state("da-foot", 1)
 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					# Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
 | 
					print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}"))
 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					machine.send_chars("exit\n")
 | 
				
			||||||
machine.wait_until_fails("pgrep foot", timeout=5)
 | 
					machine.wait_until_fails("pgrep foot", timeout=5)
 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005", timeout=5)
 | 
					machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}", timeout=5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test syscall filter:
 | 
					# Test syscall filter:
 | 
				
			||||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
					print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
				
			||||||
 | 
				
			|||||||
@ -94,7 +94,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			// keep socket alive until done is requested
 | 
								// keep socket alive until done is requested
 | 
				
			||||||
			<-done
 | 
								<-done
 | 
				
			||||||
			runtime.KeepAlive(syncPipe[1].Fd())
 | 
								runtime.KeepAlive(syncPipe[1])
 | 
				
			||||||
		}); err != nil {
 | 
							}); err != nil {
 | 
				
			||||||
			setupDone <- err
 | 
								setupDone <- err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -107,7 +107,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
 | 
				
			|||||||
	return syncPipe[1], <-setupDone
 | 
						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
 | 
						// ensure p is available
 | 
				
			||||||
	if f, err := os.Create(p); err != nil {
 | 
						if f, err := os.Create(p); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -117,5 +117,5 @@ func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
 | 
				
			|||||||
		return err
 | 
							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"),
 | 
						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) {
 | 
						if hasNull(appID) || hasNull(instanceID) {
 | 
				
			||||||
		return ErrContainsNull
 | 
							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)]
 | 
						return resErr[int32(res)]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user