Compare commits
	
		
			122 Commits
		
	
	
		
			d8e9d71f87
			...
			2a4e2724a3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2a4e2724a3 | |||
| d613257841 | |||
| 18644d90be | |||
| 52fcc48ac1 | |||
| 8b69bcd215 | |||
| 2dd49c437c | |||
| 92852d8235 | |||
| 371dd5b938 | |||
| 4836d570ae | |||
| 985f9442e6 | |||
| 67eb28466d | |||
| c326c3f97d | |||
| 971c79bb80 | |||
| f86d868274 | |||
| 33940265a6 | |||
| b39f3aeb59 | |||
| 61dbfeffe7 | |||
| 532feb4bfa | |||
| ec5e91b8c9 | |||
| ee51320abf | |||
| 5c4058d5ac | |||
| e732dca762 | |||
| a9adcd914b | |||
| 3dd4ff29c8 | |||
| 61d86c5e10 | |||
| d097eaa28f | |||
| ad3576c164 | |||
| b989a4601a | |||
| a11237b158 | |||
| 40f00d570e | |||
| 0eb1bc6301 | |||
| 1eb837eab8 | |||
| 0a4e633db2 | |||
| e8809125d4 | |||
| 806ce18c0a | |||
| b71d2bf534 | |||
| 46059b1840 | |||
| d2c329bcea | |||
| 2d379b5a38 | |||
| 75e0c5d406 | |||
| 770b37ae16 | |||
| c638193268 | |||
| 8c3a817881 | |||
| e2fce321c1 | |||
| 241702ae3a | |||
| d21d9c5b1d | |||
| a70daf2250 | |||
| 632b18addd | |||
| a57a7a6a16 | |||
| 5098b12e4a | |||
| 9ddf5794dd | |||
| b74a08dda9 | |||
| 1b9408864f | |||
| cc89dbdf63 | |||
| 228f3301f2 | |||
| 07181138e5 | |||
| 816b372f14 | |||
| d7eddd54a2 | |||
| 7c063833e0 | |||
| af3619d440 | |||
| 528674cb6e | |||
| 70c9757e26 | |||
| c83a7e2efc | |||
| 904208b87f | |||
| 007b52d81f | |||
| 3385538142 | |||
| 24618ab9a1 | |||
| 9ce4706a07 | |||
| 9a1f8e129f | |||
| ee10860357 | |||
| 44277dc0f1 | |||
| bc54db54d2 | |||
| bf07b7cd9e | |||
| 5d3c8dcc92 | |||
| 48feca800f | |||
| 42de09e896 | |||
| 1576fea8a3 | |||
| ae522ab364 | |||
| 273d97af85 | |||
| 891316d924 | |||
| 9f5dad1998 | |||
| 6e7ddb2d2e | |||
| bac4e67867 | |||
| 4230281194 | |||
| e64e7608ca | |||
| 10a21ce3ef | |||
| 0f1f0e4364 | |||
| f9bf20a3c7 | |||
| 73c1a83032 | |||
| f443d315ad | |||
| 9e18d1de77 | |||
| 2647a71be1 | |||
| 7c60a4d8e8 | |||
| 4bb5d9780f | |||
| f41fd94628 | |||
| 94895bbacb | |||
| f332200ca4 | |||
| 2eff470091 | |||
| a092b042ab | |||
| e94b09d337 | |||
| 5d9e669d97 | |||
| f1002157a5 | |||
| 4133b555ba | |||
| 9b1a60b5c9 | |||
| beb3918809 | |||
| 2871426df2 | |||
| e048f31baa | |||
| 6af8b8859f | |||
| f38ba7e923 | |||
| d22145a392 | |||
| 29c3f8becb | |||
| be16970e77 | |||
| df266527f1 | |||
| c8ed7aae6e | |||
| 61e58aa14d | |||
| 9e15898c8f | |||
| f7bd6a5a41 | |||
| ea853e21d9 | |||
| 0bd9b9e8fe | |||
| 39e32799b3 | |||
| 9953768de5 | |||
| 0d3652b793 | 
@ -4,12 +4,15 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type bundleInfo struct {
 | 
					type appInfo struct {
 | 
				
			||||||
	Name    string `json:"name"`
 | 
						Name    string `json:"name"`
 | 
				
			||||||
	Version string `json:"version"`
 | 
						Version string `json:"version"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,13 +23,15 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Groups []string `json:"groups,omitempty"`
 | 
						Groups []string `json:"groups,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
						Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
 | 
						Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
						Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
						Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
						Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
						MapRealUID bool `json:"map_real_uid,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
@ -36,13 +41,11 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
						SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
				
			||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablement `json:"enablements"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through inverted to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Devel bool `json:"devel,omitempty"`
 | 
					 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
					 | 
				
			||||||
	Multiarch bool `json:"multiarch,omitempty"`
 | 
						Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	// passed through to [bwrap.SyscallPolicy]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Bluetooth bool `json:"bluetooth,omitempty"`
 | 
						Bluetooth bool `json:"bluetooth,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// allow gpu access within sandbox
 | 
						// allow gpu access within sandbox
 | 
				
			||||||
@ -59,8 +62,64 @@ type bundleInfo struct {
 | 
				
			|||||||
	ActivationPackage string `json:"activation_package"`
 | 
						ActivationPackage string `json:"activation_package"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
 | 
					func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
 | 
				
			||||||
	bundle := new(bundleInfo)
 | 
						config := &fst.Config{
 | 
				
			||||||
 | 
							ID:   app.ID,
 | 
				
			||||||
 | 
							Path: argv[0],
 | 
				
			||||||
 | 
							Args: argv,
 | 
				
			||||||
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
 | 
								AppID:    app.AppID,
 | 
				
			||||||
 | 
								Groups:   app.Groups,
 | 
				
			||||||
 | 
								Username: "fortify",
 | 
				
			||||||
 | 
								Inner:    path.Join("/data/data", app.ID),
 | 
				
			||||||
 | 
								Outer:    pathSet.homeDir,
 | 
				
			||||||
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
 | 
									Hostname:      formatHostname(app.Name),
 | 
				
			||||||
 | 
									Devel:         app.Devel,
 | 
				
			||||||
 | 
									Userns:        app.Userns,
 | 
				
			||||||
 | 
									Net:           app.Net,
 | 
				
			||||||
 | 
									Dev:           app.Dev,
 | 
				
			||||||
 | 
									Tty:           app.Tty || flagDropShell,
 | 
				
			||||||
 | 
									MapRealUID:    app.MapRealUID,
 | 
				
			||||||
 | 
									DirectWayland: app.DirectWayland,
 | 
				
			||||||
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
 | 
										{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
 | 
				
			||||||
 | 
										{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
 | 
				
			||||||
 | 
										{Src: "/etc/resolv.conf"},
 | 
				
			||||||
 | 
										{Src: "/sys/block"},
 | 
				
			||||||
 | 
										{Src: "/sys/bus"},
 | 
				
			||||||
 | 
										{Src: "/sys/class"},
 | 
				
			||||||
 | 
										{Src: "/sys/dev"},
 | 
				
			||||||
 | 
										{Src: "/sys/devices"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Link: [][2]string{
 | 
				
			||||||
 | 
										{app.CurrentSystem, "/run/current-system"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/bin"},
 | 
				
			||||||
 | 
										{"/run/current-system/sw/bin", "/usr/bin"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Etc:     path.Join(pathSet.cacheDir, "etc"),
 | 
				
			||||||
 | 
									AutoEtc: true,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								ExtraPerms: []*fst.ExtraPermConfig{
 | 
				
			||||||
 | 
									{Path: dataHome, Execute: true},
 | 
				
			||||||
 | 
									{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								SystemBus:   app.SystemBus,
 | 
				
			||||||
 | 
								SessionBus:  app.SessionBus,
 | 
				
			||||||
 | 
								Enablements: app.Enablements,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Multiarch {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if app.Bluetooth {
 | 
				
			||||||
 | 
							config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return config
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadAppInfo(name string, beforeFail func()) *appInfo {
 | 
				
			||||||
 | 
						bundle := new(appInfo)
 | 
				
			||||||
	if f, err := os.Open(name); err != nil {
 | 
						if f, err := os.Open(name); err != nil {
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		log.Fatalf("cannot open bundle: %v", err)
 | 
							log.Fatalf("cannot open bundle: %v", err)
 | 
				
			||||||
							
								
								
									
										113
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								cmd/fpkg/main.go
									
									
									
									
									
								
							@ -12,13 +12,11 @@ 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/helper/seccomp"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	init0 "git.gensokyo.uk/security/fortify/internal/app/init"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"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"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const shellPath = "/run/current-system/sw/bin/bash"
 | 
					const shellPath = "/run/current-system/sw/bin/bash"
 | 
				
			||||||
@ -37,10 +35,10 @@ func init() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
	init0.TryArgv0()
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
		// not fatal: this program runs as the privileged user
 | 
							// not fatal: this program runs as the privileged user
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -58,18 +56,13 @@ func main() {
 | 
				
			|||||||
		flagDropShell bool
 | 
							flagDropShell bool
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
 | 
						c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
 | 
				
			||||||
		fmsg.Store(flagVerbose)
 | 
							internal.InstallFmsg(flagVerbose)
 | 
				
			||||||
		if flagVerbose {
 | 
					 | 
				
			||||||
			seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}).
 | 
						}).
 | 
				
			||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&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 { app.ShimMain(); 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 (
 | 
				
			||||||
@ -126,10 +119,10 @@ 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
 | 
								a := bundle
 | 
				
			||||||
			if s, err := os.Stat(pathSet.metaPath); err != nil {
 | 
								if s, err := os.Stat(pathSet.metaPath); err != nil {
 | 
				
			||||||
				if !os.IsNotExist(err) {
 | 
									if !os.IsNotExist(err) {
 | 
				
			||||||
					cleanup()
 | 
										cleanup()
 | 
				
			||||||
@ -142,39 +135,39 @@ 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)
 | 
									a = loadAppInfo(pathSet.metaPath, cleanup)
 | 
				
			||||||
				if app.ID != bundle.ID {
 | 
									if a.ID != bundle.ID {
 | 
				
			||||||
					cleanup()
 | 
										cleanup()
 | 
				
			||||||
					log.Printf("app %q claims to have identifier %q",
 | 
										log.Printf("app %q claims to have identifier %q",
 | 
				
			||||||
						bundle.ID, app.ID)
 | 
											bundle.ID, a.ID)
 | 
				
			||||||
					return syscall.EBADE
 | 
										return syscall.EBADE
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// sec: should verify credentials
 | 
									// sec: should verify credentials
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if app != bundle {
 | 
								if a != bundle {
 | 
				
			||||||
				// do not try to re-install
 | 
									// do not try to re-install
 | 
				
			||||||
				if app.NixGL == bundle.NixGL &&
 | 
									if a.NixGL == bundle.NixGL &&
 | 
				
			||||||
					app.CurrentSystem == bundle.CurrentSystem &&
 | 
										a.CurrentSystem == bundle.CurrentSystem &&
 | 
				
			||||||
					app.Launcher == bundle.Launcher &&
 | 
										a.Launcher == bundle.Launcher &&
 | 
				
			||||||
					app.ActivationPackage == bundle.ActivationPackage {
 | 
										a.ActivationPackage == bundle.ActivationPackage {
 | 
				
			||||||
					cleanup()
 | 
										cleanup()
 | 
				
			||||||
					log.Printf("package %q is identical to local application %q",
 | 
										log.Printf("package %q is identical to local application %q",
 | 
				
			||||||
						pkgPath, app.ID)
 | 
											pkgPath, a.ID)
 | 
				
			||||||
					return errSuccess
 | 
										return errSuccess
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// AppID determines uid
 | 
									// AppID determines uid
 | 
				
			||||||
				if app.AppID != bundle.AppID {
 | 
									if a.AppID != bundle.AppID {
 | 
				
			||||||
					cleanup()
 | 
										cleanup()
 | 
				
			||||||
					log.Printf("package %q app id %d differs from installed %d",
 | 
										log.Printf("package %q app id %d differs from installed %d",
 | 
				
			||||||
						pkgPath, bundle.AppID, app.AppID)
 | 
											pkgPath, bundle.AppID, a.AppID)
 | 
				
			||||||
					return syscall.EBADE
 | 
										return syscall.EBADE
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// sec: should compare version string
 | 
									// sec: should compare version string
 | 
				
			||||||
				fmsg.Verbosef("installing application %q version %q over local %q",
 | 
									fmsg.Verbosef("installing application %q version %q over local %q",
 | 
				
			||||||
					bundle.ID, bundle.Version, app.Version)
 | 
										bundle.ID, bundle.Version, a.Version)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				fmsg.Verbosef("application %q clean installation", bundle.ID)
 | 
									fmsg.Verbosef("application %q clean installation", bundle.ID)
 | 
				
			||||||
				// sec: should install credentials
 | 
									// sec: should install credentials
 | 
				
			||||||
@ -275,9 +268,9 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			id := args[0]
 | 
								id := args[0]
 | 
				
			||||||
			pathSet := pathSetByApp(id)
 | 
								pathSet := pathSetByApp(id)
 | 
				
			||||||
			app := loadBundleInfo(pathSet.metaPath, func() {})
 | 
								a := loadAppInfo(pathSet.metaPath, func() {})
 | 
				
			||||||
			if app.ID != id {
 | 
								if a.ID != id {
 | 
				
			||||||
				log.Printf("app %q claims to have identifier %q", id, app.ID)
 | 
									log.Printf("app %q claims to have identifier %q", id, a.ID)
 | 
				
			||||||
				return syscall.EBADE
 | 
									return syscall.EBADE
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -285,7 +278,7 @@ func main() {
 | 
				
			|||||||
				Prepare nixGL.
 | 
									Prepare nixGL.
 | 
				
			||||||
			*/
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if app.GPU && flagAutoDrivers {
 | 
								if a.GPU && flagAutoDrivers {
 | 
				
			||||||
				withNixDaemon(ctx, "nix-gl", []string{
 | 
									withNixDaemon(ctx, "nix-gl", []string{
 | 
				
			||||||
					"mkdir -p /nix/.nixGL/auto",
 | 
										"mkdir -p /nix/.nixGL/auto",
 | 
				
			||||||
					"rm -rf /nix/.nixGL/auto",
 | 
										"rm -rf /nix/.nixGL/auto",
 | 
				
			||||||
@ -293,11 +286,11 @@ func main() {
 | 
				
			|||||||
					"nix build --impure " +
 | 
										"nix build --impure " +
 | 
				
			||||||
						"--out-link /nix/.nixGL/auto/opengl " +
 | 
											"--out-link /nix/.nixGL/auto/opengl " +
 | 
				
			||||||
						"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
											"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
				
			||||||
						"path:" + app.NixGL,
 | 
											"path:" + a.NixGL,
 | 
				
			||||||
					"nix build --impure " +
 | 
										"nix build --impure " +
 | 
				
			||||||
						"--out-link /nix/.nixGL/auto/vulkan " +
 | 
											"--out-link /nix/.nixGL/auto/vulkan " +
 | 
				
			||||||
						"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
											"--override-input nixpkgs path:/etc/nixpkgs " +
 | 
				
			||||||
						"path:" + app.NixGL + "#nixVulkanNvidia",
 | 
											"path:" + a.NixGL + "#nixVulkanNvidia",
 | 
				
			||||||
				}, true, func(config *fst.Config) *fst.Config {
 | 
									}, true, func(config *fst.Config) *fst.Config {
 | 
				
			||||||
					config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
										config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
 | 
				
			||||||
						{Src: "/etc/resolv.conf"},
 | 
											{Src: "/etc/resolv.conf"},
 | 
				
			||||||
@ -309,7 +302,7 @@ func main() {
 | 
				
			|||||||
					}...)
 | 
										}...)
 | 
				
			||||||
					appendGPUFilesystem(config)
 | 
										appendGPUFilesystem(config)
 | 
				
			||||||
					return config
 | 
										return config
 | 
				
			||||||
				}, app, pathSet, flagDropShellNixGL, func() {})
 | 
									}, a, pathSet, flagDropShellNixGL, func() {})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/*
 | 
								/*
 | 
				
			||||||
@ -318,63 +311,19 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			argv := make([]string, 1, len(args))
 | 
								argv := make([]string, 1, len(args))
 | 
				
			||||||
			if !flagDropShell {
 | 
								if !flagDropShell {
 | 
				
			||||||
				argv[0] = app.Launcher
 | 
									argv[0] = a.Launcher
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				argv[0] = shellPath
 | 
									argv[0] = shellPath
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			argv = append(argv, args[1:]...)
 | 
								argv = append(argv, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			config := &fst.Config{
 | 
								config := a.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.
 | 
				
			||||||
			*/
 | 
								*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if app.GPU {
 | 
								if a.GPU {
 | 
				
			||||||
				config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
 | 
									config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
 | 
				
			||||||
					&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
 | 
										&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
 | 
				
			||||||
				appendGPUFilesystem(config)
 | 
									appendGPUFilesystem(config)
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@
 | 
				
			|||||||
      echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
 | 
					      echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      sway --validate
 | 
					      sway --validate
 | 
				
			||||||
      systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
 | 
					      systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
  '';
 | 
					  '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,21 @@
 | 
				
			|||||||
package dbus_test
 | 
					package dbus_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestNew(t *testing.T) {
 | 
					func TestNew(t *testing.T) {
 | 
				
			||||||
@ -100,15 +107,20 @@ func TestProxy_Seal(t *testing.T) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
 | 
					func TestProxy_Start_Wait_Close_String(t *testing.T) {
 | 
				
			||||||
	t.Run("sandboxed", func(t *testing.T) {
 | 
						oldWaitDelay := helper.WaitDelay
 | 
				
			||||||
 | 
						helper.WaitDelay = 16 * time.Second
 | 
				
			||||||
 | 
						t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("sandbox", func(t *testing.T) {
 | 
				
			||||||
 | 
							proxyName := dbus.ProxyName
 | 
				
			||||||
 | 
							dbus.ProxyName = os.Args[0]
 | 
				
			||||||
 | 
							t.Cleanup(func() { dbus.ProxyName = proxyName })
 | 
				
			||||||
		testProxyStartWaitCloseString(t, true)
 | 
							testProxyStartWaitCloseString(t, true)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	t.Run("direct", func(t *testing.T) {
 | 
						t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
 | 
				
			||||||
		testProxyStartWaitCloseString(t, false)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
					func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
 | 
				
			||||||
	for id, tc := range testCasePairs() {
 | 
						for id, tc := range testCasePairs() {
 | 
				
			||||||
		// this test does not test errors
 | 
							// this test does not test errors
 | 
				
			||||||
		if tc[0].wantErr {
 | 
							if tc[0].wantErr {
 | 
				
			||||||
@ -125,14 +137,33 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("proxy for "+id, func(t *testing.T) {
 | 
							t.Run("proxy for "+id, func(t *testing.T) {
 | 
				
			||||||
			helper.InternalReplaceExecCommand(t)
 | 
					 | 
				
			||||||
			overridePath(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			p := dbus.New(tc[0].bus, tc[1].bus)
 | 
								p := dbus.New(tc[0].bus, tc[1].bus)
 | 
				
			||||||
 | 
								p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
 | 
				
			||||||
 | 
									return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
										"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.CmdF = func(v any) {
 | 
				
			||||||
 | 
									if useSandbox {
 | 
				
			||||||
 | 
										container := v.(*sandbox.Container)
 | 
				
			||||||
 | 
										if container.Args[0] != dbus.ProxyName {
 | 
				
			||||||
 | 
											panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										cmd := v.(*exec.Cmd)
 | 
				
			||||||
 | 
										if cmd.Args[0] != dbus.ProxyName {
 | 
				
			||||||
 | 
											panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										cmd.Err = nil
 | 
				
			||||||
 | 
										cmd.Path = os.Args[0]
 | 
				
			||||||
 | 
										cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
 | 
				
			||||||
			output := new(strings.Builder)
 | 
								output := new(strings.Builder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("unsealed behaviour of "+id, func(t *testing.T) {
 | 
								t.Run("unsealed", func(t *testing.T) {
 | 
				
			||||||
				t.Run("unsealed string of "+id, func(t *testing.T) {
 | 
									t.Run("string", func(t *testing.T) {
 | 
				
			||||||
					want := "(unsealed dbus proxy)"
 | 
										want := "(unsealed dbus proxy)"
 | 
				
			||||||
					if got := p.String(); got != want {
 | 
										if got := p.String(); got != want {
 | 
				
			||||||
						t.Errorf("String() = %v, want %v",
 | 
											t.Errorf("String() = %v, want %v",
 | 
				
			||||||
@ -141,16 +172,16 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("unsealed start of "+id, func(t *testing.T) {
 | 
									t.Run("start", func(t *testing.T) {
 | 
				
			||||||
					want := "proxy not sealed"
 | 
										want := "proxy not sealed"
 | 
				
			||||||
					if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
 | 
										if err := p.Start(context.Background(), nil, useSandbox); err == nil || err.Error() != want {
 | 
				
			||||||
						t.Errorf("Start() error = %v, wantErr %q",
 | 
											t.Errorf("Start() error = %v, wantErr %q",
 | 
				
			||||||
							err, errors.New(want))
 | 
												err, errors.New(want))
 | 
				
			||||||
						return
 | 
											return
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("unsealed wait of "+id, func(t *testing.T) {
 | 
									t.Run("wait", func(t *testing.T) {
 | 
				
			||||||
					wantErr := "dbus: not started"
 | 
										wantErr := "dbus: not started"
 | 
				
			||||||
					if err := p.Wait(); err == nil || err.Error() != wantErr {
 | 
										if err := p.Wait(); err == nil || err.Error() != wantErr {
 | 
				
			||||||
						t.Errorf("Wait() error = %v, wantErr %v",
 | 
											t.Errorf("Wait() error = %v, wantErr %v",
 | 
				
			||||||
@ -168,7 +199,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("sealed behaviour of "+id, func(t *testing.T) {
 | 
								t.Run("sealed", func(t *testing.T) {
 | 
				
			||||||
				want := strings.Join(append(tc[0].want, tc[1].want...), " ")
 | 
									want := strings.Join(append(tc[0].want, tc[1].want...), " ")
 | 
				
			||||||
				if got := p.String(); got != want {
 | 
									if got := p.String(); got != want {
 | 
				
			||||||
					t.Errorf("String() = %v, want %v",
 | 
										t.Errorf("String() = %v, want %v",
 | 
				
			||||||
@ -176,17 +207,20 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
					return
 | 
										return
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("sealed start of "+id, func(t *testing.T) {
 | 
									t.Run("start", func(t *testing.T) {
 | 
				
			||||||
					ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
										ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
					defer cancel()
 | 
										defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					if err := p.Start(ctx, output, sandbox); err != nil {
 | 
										if err := p.Start(ctx, output, useSandbox); err != nil {
 | 
				
			||||||
						t.Fatalf("Start(nil, nil) error = %v",
 | 
											t.Fatalf("Start(nil, nil) error = %v",
 | 
				
			||||||
							err)
 | 
												err)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					t.Run("started string of "+id, func(t *testing.T) {
 | 
										t.Run("string", func(t *testing.T) {
 | 
				
			||||||
						wantSubstr := dbus.ProxyName + " --args="
 | 
											wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
 | 
				
			||||||
 | 
											if useSandbox {
 | 
				
			||||||
 | 
												wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x3e`, os.Args[0])
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
						if got := p.String(); !strings.Contains(got, wantSubstr) {
 | 
											if got := p.String(); !strings.Contains(got, wantSubstr) {
 | 
				
			||||||
							t.Errorf("String() = %v, want %v",
 | 
												t.Errorf("String() = %v, want %v",
 | 
				
			||||||
								p.String(), wantSubstr)
 | 
													p.String(), wantSubstr)
 | 
				
			||||||
@ -194,7 +228,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
						}
 | 
											}
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					t.Run("started wait of "+id, func(t *testing.T) {
 | 
										t.Run("wait", func(t *testing.T) {
 | 
				
			||||||
						p.Close()
 | 
											p.Close()
 | 
				
			||||||
						if err := p.Wait(); err != nil {
 | 
											if err := p.Wait(); err != nil {
 | 
				
			||||||
							t.Errorf("Wait() error = %v\noutput: %s",
 | 
												t.Errorf("Wait() error = %v\noutput: %s",
 | 
				
			||||||
@ -207,10 +241,10 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func overridePath(t *testing.T) {
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
	proxyName := dbus.ProxyName
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
	dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
 | 
							return
 | 
				
			||||||
	t.Cleanup(func() {
 | 
						}
 | 
				
			||||||
		dbus.ProxyName = proxyName
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
	})
 | 
						sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										178
									
								
								dbus/proc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								dbus/proc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					package dbus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Start launches the D-Bus proxy.
 | 
				
			||||||
 | 
					func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) error {
 | 
				
			||||||
 | 
						p.lock.Lock()
 | 
				
			||||||
 | 
						defer p.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.seal == nil {
 | 
				
			||||||
 | 
							return errors.New("proxy not sealed")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var h helper.Helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, cancel := context.WithCancelCause(ctx)
 | 
				
			||||||
 | 
						if !useSandbox {
 | 
				
			||||||
 | 
							h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
 | 
				
			||||||
 | 
								if p.CmdF != nil {
 | 
				
			||||||
 | 
									p.CmdF(cmd)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if output != nil {
 | 
				
			||||||
 | 
									cmd.Stdout, cmd.Stderr = output, output
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
				
			||||||
 | 
								cmd.Env = make([]string, 0)
 | 
				
			||||||
 | 
							}, nil)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							toolPath := p.name
 | 
				
			||||||
 | 
							if filepath.Base(p.name) == p.name {
 | 
				
			||||||
 | 
								if s, err := exec.LookPath(p.name); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									toolPath = s
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var libPaths []string
 | 
				
			||||||
 | 
							if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								libPaths = ldd.Path(entries)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h = helper.New(
 | 
				
			||||||
 | 
								c, toolPath,
 | 
				
			||||||
 | 
								p.seal, true,
 | 
				
			||||||
 | 
								argF, func(container *sandbox.Container) {
 | 
				
			||||||
 | 
									container.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
									container.Hostname = "fortify-dbus"
 | 
				
			||||||
 | 
									container.CommandContext = p.CommandContext
 | 
				
			||||||
 | 
									if output != nil {
 | 
				
			||||||
 | 
										container.Stdout, container.Stderr = output, output
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if p.CmdF != nil {
 | 
				
			||||||
 | 
										p.CmdF(container)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// these lib paths are unpredictable, so mount them first so they cannot cover anything
 | 
				
			||||||
 | 
									for _, name := range libPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, 0)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// upstream bus directories
 | 
				
			||||||
 | 
									upstreamPaths := make([]string, 0, 2)
 | 
				
			||||||
 | 
									for _, as := range []string{p.session[0], p.system[0]} {
 | 
				
			||||||
 | 
										if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
 | 
				
			||||||
 | 
											// leave / intact
 | 
				
			||||||
 | 
											upstreamPaths = append(upstreamPaths, path.Dir(as[10:]))
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									slices.Sort(upstreamPaths)
 | 
				
			||||||
 | 
									upstreamPaths = slices.Compact(upstreamPaths)
 | 
				
			||||||
 | 
									for _, name := range upstreamPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, 0)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// parent directories of bind paths
 | 
				
			||||||
 | 
									sockDirPaths := make([]string, 0, 2)
 | 
				
			||||||
 | 
									if d := path.Dir(p.session[1]); path.IsAbs(d) {
 | 
				
			||||||
 | 
										sockDirPaths = append(sockDirPaths, d)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if d := path.Dir(p.system[1]); path.IsAbs(d) {
 | 
				
			||||||
 | 
										sockDirPaths = append(sockDirPaths, d)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									slices.Sort(sockDirPaths)
 | 
				
			||||||
 | 
									sockDirPaths = slices.Compact(sockDirPaths)
 | 
				
			||||||
 | 
									for _, name := range sockDirPaths {
 | 
				
			||||||
 | 
										container.Bind(name, name, sandbox.BindWritable)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// xdg-dbus-proxy bin path
 | 
				
			||||||
 | 
									binPath := path.Dir(toolPath)
 | 
				
			||||||
 | 
									container.Bind(binPath, binPath, 0)
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := h.Start(); err != nil {
 | 
				
			||||||
 | 
							cancel(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p.helper = h
 | 
				
			||||||
 | 
						p.ctx = c
 | 
				
			||||||
 | 
						p.cancel = cancel
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var proxyClosed = errors.New("proxy closed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Wait blocks until xdg-dbus-proxy exits and releases resources.
 | 
				
			||||||
 | 
					func (p *Proxy) Wait() error {
 | 
				
			||||||
 | 
						p.lock.RLock()
 | 
				
			||||||
 | 
						defer p.lock.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.helper == nil {
 | 
				
			||||||
 | 
							return errors.New("dbus: not started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errs := make([]error, 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errs[0] = p.helper.Wait()
 | 
				
			||||||
 | 
						if p.cancel == nil &&
 | 
				
			||||||
 | 
							errors.Is(errs[0], context.Canceled) &&
 | 
				
			||||||
 | 
							errors.Is(context.Cause(p.ctx), proxyClosed) {
 | 
				
			||||||
 | 
							errs[0] = nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ensure socket removal so ephemeral directory is empty at revert
 | 
				
			||||||
 | 
						if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
							errs[1] = err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.sysP {
 | 
				
			||||||
 | 
							if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
								errs[2] = err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return errors.Join(errs...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
 | 
				
			||||||
 | 
					func (p *Proxy) Close() {
 | 
				
			||||||
 | 
						p.lock.Lock()
 | 
				
			||||||
 | 
						defer p.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.cancel == nil {
 | 
				
			||||||
 | 
							panic("dbus: not started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cancel(proxyClosed)
 | 
				
			||||||
 | 
						p.cancel = nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func argF(argsFd, statFd int) []string {
 | 
				
			||||||
 | 
						if statFd == -1 {
 | 
				
			||||||
 | 
							return []string{"--args=" + strconv.Itoa(argsFd)}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -5,10 +5,10 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ProxyName is the file name or path to the proxy program.
 | 
					// ProxyName is the file name or path to the proxy program.
 | 
				
			||||||
@ -19,15 +19,18 @@ var ProxyName = "xdg-dbus-proxy"
 | 
				
			|||||||
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
 | 
					// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
 | 
				
			||||||
type Proxy struct {
 | 
					type Proxy struct {
 | 
				
			||||||
	helper helper.Helper
 | 
						helper helper.Helper
 | 
				
			||||||
	bwrap  *bwrap.Config
 | 
					 | 
				
			||||||
	ctx    context.Context
 | 
						ctx    context.Context
 | 
				
			||||||
	cancel context.CancelCauseFunc
 | 
						cancel context.CancelCauseFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	name    string
 | 
						name    string
 | 
				
			||||||
	session [2]string
 | 
						session [2]string
 | 
				
			||||||
	system  [2]string
 | 
						system  [2]string
 | 
				
			||||||
 | 
						CmdF    func(any)
 | 
				
			||||||
	sysP    bool
 | 
						sysP    bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
				
			||||||
 | 
						FilterF        func([]byte) []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	seal io.WriterTo
 | 
						seal io.WriterTo
 | 
				
			||||||
	lock sync.RWMutex
 | 
						lock sync.RWMutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										175
									
								
								dbus/run.go
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								dbus/run.go
									
									
									
									
									
								
							@ -1,175 +0,0 @@
 | 
				
			|||||||
package dbus
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Start launches the D-Bus proxy.
 | 
					 | 
				
			||||||
func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
 | 
					 | 
				
			||||||
	p.lock.Lock()
 | 
					 | 
				
			||||||
	defer p.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.seal == nil {
 | 
					 | 
				
			||||||
		return errors.New("proxy not sealed")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		h helper.Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		argF = func(argsFD, statFD int) []string {
 | 
					 | 
				
			||||||
			if statFD == -1 {
 | 
					 | 
				
			||||||
				return []string{"--args=" + strconv.Itoa(argsFD)}
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				return []string{"--args=" + strconv.Itoa(argsFD), "--fd=" + strconv.Itoa(statFD)}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !sandbox {
 | 
					 | 
				
			||||||
		h = helper.New(p.seal, p.name, argF)
 | 
					 | 
				
			||||||
		// xdg-dbus-proxy does not need to inherit the environment
 | 
					 | 
				
			||||||
		h.SetEnv(make([]string, 0))
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// look up absolute path if name is just a file name
 | 
					 | 
				
			||||||
		toolPath := p.name
 | 
					 | 
				
			||||||
		if filepath.Base(p.name) == p.name {
 | 
					 | 
				
			||||||
			if s, err := exec.LookPath(p.name); err != nil {
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				toolPath = s
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve libraries by parsing ldd output
 | 
					 | 
				
			||||||
		var proxyDeps []*ldd.Entry
 | 
					 | 
				
			||||||
		if toolPath != "/nonexistent-xdg-dbus-proxy" {
 | 
					 | 
				
			||||||
			if l, err := ldd.Exec(ctx, toolPath); err != nil {
 | 
					 | 
				
			||||||
				return err
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				proxyDeps = l
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		bc := &bwrap.Config{
 | 
					 | 
				
			||||||
			Unshare:       nil,
 | 
					 | 
				
			||||||
			Hostname:      "fortify-dbus",
 | 
					 | 
				
			||||||
			Chdir:         "/",
 | 
					 | 
				
			||||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
					 | 
				
			||||||
			Clearenv:      true,
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve proxy socket directories
 | 
					 | 
				
			||||||
		bindTarget := make(map[string]struct{}, 2)
 | 
					 | 
				
			||||||
		for _, ps := range []string{p.session[1], p.system[1]} {
 | 
					 | 
				
			||||||
			if pd := path.Dir(ps); len(pd) > 0 {
 | 
					 | 
				
			||||||
				if pd[0] == '/' {
 | 
					 | 
				
			||||||
					bindTarget[pd] = struct{}{}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		for k := range bindTarget {
 | 
					 | 
				
			||||||
			bc.Bind(k, k, false, true)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// xdb-dbus-proxy bin and dependencies
 | 
					 | 
				
			||||||
		roBindTarget[path.Dir(toolPath)] = struct{}{}
 | 
					 | 
				
			||||||
		for _, ent := range proxyDeps {
 | 
					 | 
				
			||||||
			if path.IsAbs(ent.Path) {
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(ent.Path)] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if path.IsAbs(ent.Name) {
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(ent.Name)] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// resolve upstream bus directories
 | 
					 | 
				
			||||||
		for _, as := range []string{p.session[0], p.system[0]} {
 | 
					 | 
				
			||||||
			if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
 | 
					 | 
				
			||||||
				// leave / intact
 | 
					 | 
				
			||||||
				roBindTarget[path.Dir(as[10:])] = struct{}{}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for k := range roBindTarget {
 | 
					 | 
				
			||||||
			bc.Bind(k, k)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h = helper.MustNewBwrap(bc, toolPath, true, p.seal, argF, nil, nil)
 | 
					 | 
				
			||||||
		p.bwrap = bc
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if output != nil {
 | 
					 | 
				
			||||||
		h.Stdout(output).Stderr(output)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c, cancel := context.WithCancelCause(ctx)
 | 
					 | 
				
			||||||
	if err := h.Start(c, true); err != nil {
 | 
					 | 
				
			||||||
		cancel(err)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	p.helper = h
 | 
					 | 
				
			||||||
	p.ctx = c
 | 
					 | 
				
			||||||
	p.cancel = cancel
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var proxyClosed = errors.New("proxy closed")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Wait blocks until xdg-dbus-proxy exits and releases resources.
 | 
					 | 
				
			||||||
func (p *Proxy) Wait() error {
 | 
					 | 
				
			||||||
	p.lock.RLock()
 | 
					 | 
				
			||||||
	defer p.lock.RUnlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.helper == nil {
 | 
					 | 
				
			||||||
		return errors.New("dbus: not started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	errs := make([]error, 3)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	errs[0] = p.helper.Wait()
 | 
					 | 
				
			||||||
	if p.cancel == nil &&
 | 
					 | 
				
			||||||
		errors.Is(errs[0], context.Canceled) &&
 | 
					 | 
				
			||||||
		errors.Is(context.Cause(p.ctx), proxyClosed) {
 | 
					 | 
				
			||||||
		errs[0] = nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ensure socket removal so ephemeral directory is empty at revert
 | 
					 | 
				
			||||||
	if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
		errs[1] = err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if p.sysP {
 | 
					 | 
				
			||||||
		if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			errs[2] = err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return errors.Join(errs...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
 | 
					 | 
				
			||||||
func (p *Proxy) Close() {
 | 
					 | 
				
			||||||
	p.lock.Lock()
 | 
					 | 
				
			||||||
	defer p.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if p.cancel == nil {
 | 
					 | 
				
			||||||
		panic("dbus: not started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	p.cancel(proxyClosed)
 | 
					 | 
				
			||||||
	p.cancel = nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -6,6 +6,12 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						sampleHostPath = "/tmp/bus"
 | 
				
			||||||
 | 
						sampleHostAddr = "unix:path=" + sampleHostPath
 | 
				
			||||||
 | 
						sampleBindPath = "/tmp/proxied_bus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var samples = []dbusTestCase{
 | 
					var samples = []dbusTestCase{
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		"org.chromium.Chromium", &dbus.Config{
 | 
							"org.chromium.Chromium", &dbus.Config{
 | 
				
			||||||
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
			"--talk=org.freedesktop.FileManager1",
 | 
								"--talk=org.freedesktop.FileManager1",
 | 
				
			||||||
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{"unix:path=/run/dbus/system_bus_socket",
 | 
							[]string{
 | 
				
			||||||
			"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
 | 
								sampleHostAddr,
 | 
				
			||||||
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.bluez",
 | 
								"--talk=org.bluez",
 | 
				
			||||||
			"--talk=org.freedesktop.Avahi",
 | 
								"--talk=org.freedesktop.Avahi",
 | 
				
			||||||
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       false,
 | 
								Log:       false,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
			"--talk=org.kde.StatusNotifierWatcher",
 | 
								"--talk=org.kde.StatusNotifierWatcher",
 | 
				
			||||||
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       true,
 | 
								Log:       true,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, false,
 | 
							}, false, false,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--see=uk.gensokyo.CrashTestDummy1",
 | 
								"--see=uk.gensokyo.CrashTestDummy1",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
 | 
				
			|||||||
			Log:       true,
 | 
								Log:       true,
 | 
				
			||||||
			Filter:    true,
 | 
								Filter:    true,
 | 
				
			||||||
		}, false, true,
 | 
							}, false, true,
 | 
				
			||||||
		[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
 | 
							[2]string{sampleHostAddr, sampleBindPath},
 | 
				
			||||||
		[]string{
 | 
							[]string{
 | 
				
			||||||
			"unix:path=/run/user/1971/bus",
 | 
								sampleHostAddr,
 | 
				
			||||||
			"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
 | 
								sampleBindPath,
 | 
				
			||||||
			"--filter",
 | 
								"--filter",
 | 
				
			||||||
			"--see=uk.gensokyo.CrashTestDummy",
 | 
								"--see=uk.gensokyo.CrashTestDummy",
 | 
				
			||||||
			"--talk=org.freedesktop.Notifications",
 | 
								"--talk=org.freedesktop.Notifications",
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,4 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestHelperChildStub(t *testing.T) {
 | 
					func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
 | 
				
			||||||
	helper.InternalChildStub()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@ -7,11 +7,11 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1736373539,
 | 
					        "lastModified": 1742234739,
 | 
				
			||||||
        "narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
 | 
					        "narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
 | 
				
			||||||
        "owner": "nix-community",
 | 
					        "owner": "nix-community",
 | 
				
			||||||
        "repo": "home-manager",
 | 
					        "repo": "home-manager",
 | 
				
			||||||
        "rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
 | 
					        "rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
@ -23,16 +23,16 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "nixpkgs": {
 | 
					    "nixpkgs": {
 | 
				
			||||||
      "locked": {
 | 
					      "locked": {
 | 
				
			||||||
        "lastModified": 1739333913,
 | 
					        "lastModified": 1742512142,
 | 
				
			||||||
        "narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
 | 
					        "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
 | 
				
			||||||
        "owner": "NixOS",
 | 
					        "owner": "NixOS",
 | 
				
			||||||
        "repo": "nixpkgs",
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
        "rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
 | 
					        "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "original": {
 | 
					      "original": {
 | 
				
			||||||
        "owner": "NixOS",
 | 
					        "owner": "NixOS",
 | 
				
			||||||
        "ref": "nixos-24.11-small",
 | 
					        "ref": "nixos-24.11",
 | 
				
			||||||
        "repo": "nixpkgs",
 | 
					        "repo": "nixpkgs",
 | 
				
			||||||
        "type": "github"
 | 
					        "type": "github"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										103
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								flake.nix
									
									
									
									
									
								
							@ -2,7 +2,7 @@
 | 
				
			|||||||
  description = "fortify sandbox tool and nixos module";
 | 
					  description = "fortify sandbox tool and nixos module";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  inputs = {
 | 
					  inputs = {
 | 
				
			||||||
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
 | 
					    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    home-manager = {
 | 
					    home-manager = {
 | 
				
			||||||
      url = "github:nix-community/home-manager/release-24.11";
 | 
					      url = "github:nix-community/home-manager/release-24.11";
 | 
				
			||||||
@ -27,7 +27,7 @@
 | 
				
			|||||||
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
					      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
 | 
				
			||||||
    in
 | 
					    in
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      nixosModules.fortify = import ./nixos.nix;
 | 
					      nixosModules.fortify = import ./nixos.nix self.packages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      buildPackage = forAllSystems (
 | 
					      buildPackage = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
@ -105,9 +105,20 @@
 | 
				
			|||||||
          default = fortify;
 | 
					          default = fortify;
 | 
				
			||||||
          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
					          fortify = pkgs.pkgsStatic.callPackage ./package.nix {
 | 
				
			||||||
            inherit (pkgs)
 | 
					            inherit (pkgs)
 | 
				
			||||||
              bubblewrap
 | 
					              # passthru.buildInputs
 | 
				
			||||||
              xdg-dbus-proxy
 | 
					              go
 | 
				
			||||||
 | 
					              gcc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # nativeBuildInputs
 | 
				
			||||||
 | 
					              pkg-config
 | 
				
			||||||
 | 
					              wayland-scanner
 | 
				
			||||||
 | 
					              makeBinaryWrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # appPackages
 | 
				
			||||||
              glibc
 | 
					              glibc
 | 
				
			||||||
 | 
					              xdg-dbus-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              # fpkg
 | 
				
			||||||
              zstd
 | 
					              zstd
 | 
				
			||||||
              gnutar
 | 
					              gnutar
 | 
				
			||||||
              coreutils
 | 
					              coreutils
 | 
				
			||||||
@ -115,7 +126,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 +139,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 +162,7 @@
 | 
				
			|||||||
                    specialArgs = {
 | 
					                    specialArgs = {
 | 
				
			||||||
                      inherit pkgs;
 | 
					                      inherit pkgs;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    modules = [ ./options.nix ];
 | 
					                    modules = [ (import ./options.nix self.packages) ];
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
					                  cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
@ -233,7 +172,7 @@
 | 
				
			|||||||
                sed -i '/*Declared by:*/,+1 d' $out
 | 
					                sed -i '/*Declared by:*/,+1 d' $out
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
            nixpkgsFor.${system}.mkShell {
 | 
					            pkgs.mkShell {
 | 
				
			||||||
              shellHook = ''
 | 
					              shellHook = ''
 | 
				
			||||||
                exec cat ${docText} > options.md
 | 
					                exec cat ${docText} > options.md
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
package fst
 | 
					package fst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +18,7 @@ type App interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type SealedApp interface {
 | 
					type SealedApp interface {
 | 
				
			||||||
	// Run commits sealed system setup and starts the app process.
 | 
						// Run commits sealed system setup and starts the app process.
 | 
				
			||||||
	Run(ctx context.Context, rs *RunState) error
 | 
						Run(rs *RunState) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RunState stores the outcome of a call to [SealedApp.Run].
 | 
					// RunState stores the outcome of a call to [SealedApp.Run].
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ package fst
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,8 +14,11 @@ type Config struct {
 | 
				
			|||||||
	// passed to wayland security-context-v1 as application ID
 | 
						// passed to wayland security-context-v1 as application ID
 | 
				
			||||||
	// and used as part of defaults in dbus session proxy
 | 
						// and used as part of defaults in dbus session proxy
 | 
				
			||||||
	ID string `json:"id"`
 | 
						ID string `json:"id"`
 | 
				
			||||||
	// final argv, passed to init
 | 
					
 | 
				
			||||||
	Command []string `json:"command"`
 | 
						// absolute path to executable file
 | 
				
			||||||
 | 
						Path string `json:"path,omitempty"`
 | 
				
			||||||
 | 
						// final args passed to container init
 | 
				
			||||||
 | 
						Args []string `json:"args"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Confinement ConfinementConfig `json:"confinement"`
 | 
						Confinement ConfinementConfig `json:"confinement"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -26,13 +29,13 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	AppID int `json:"app_id"`
 | 
						AppID int `json:"app_id"`
 | 
				
			||||||
	// list of supplementary groups to inherit
 | 
						// list of supplementary groups to inherit
 | 
				
			||||||
	Groups []string `json:"groups"`
 | 
						Groups []string `json:"groups"`
 | 
				
			||||||
	// passwd username in the sandbox, defaults to passwd name of target uid or chronos
 | 
						// passwd username in container, defaults to passwd name of target uid or chronos
 | 
				
			||||||
	Username string `json:"username,omitempty"`
 | 
						Username string `json:"username,omitempty"`
 | 
				
			||||||
	// home directory in sandbox, empty for outer
 | 
						// home directory in container, empty for outer
 | 
				
			||||||
	Inner string `json:"home_inner"`
 | 
						Inner string `json:"home_inner"`
 | 
				
			||||||
	// home directory in init namespace
 | 
						// home directory in init namespace
 | 
				
			||||||
	Outer string `json:"home"`
 | 
						Outer string `json:"home"`
 | 
				
			||||||
	// bwrap sandbox confinement configuration
 | 
						// abstract sandbox configuration
 | 
				
			||||||
	Sandbox *SandboxConfig `json:"sandbox"`
 | 
						Sandbox *SandboxConfig `json:"sandbox"`
 | 
				
			||||||
	// extra acl ops, runs after everything else
 | 
						// extra acl ops, runs after everything else
 | 
				
			||||||
	ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
						ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
 | 
				
			||||||
@ -44,8 +47,8 @@ type ConfinementConfig struct {
 | 
				
			|||||||
	// nil value makes session bus proxy assume built-in defaults
 | 
						// nil value makes session bus proxy assume built-in defaults
 | 
				
			||||||
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
						SessionBus *dbus.Config `json:"session_bus,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// system resources to expose to the sandbox
 | 
						// system resources to expose to the container
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablement `json:"enablements"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ExtraPermConfig struct {
 | 
					type ExtraPermConfig struct {
 | 
				
			||||||
@ -76,24 +79,12 @@ func (e *ExtraPermConfig) String() string {
 | 
				
			|||||||
	return string(buf)
 | 
						return string(buf)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FilesystemConfig struct {
 | 
					 | 
				
			||||||
	// mount point in sandbox, same as src if empty
 | 
					 | 
				
			||||||
	Dst string `json:"dst,omitempty"`
 | 
					 | 
				
			||||||
	// host filesystem path to make available to sandbox
 | 
					 | 
				
			||||||
	Src string `json:"src"`
 | 
					 | 
				
			||||||
	// write access
 | 
					 | 
				
			||||||
	Write bool `json:"write,omitempty"`
 | 
					 | 
				
			||||||
	// device access
 | 
					 | 
				
			||||||
	Device bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// fail if mount fails
 | 
					 | 
				
			||||||
	Must bool `json:"require,omitempty"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Template returns a fully populated instance of Config.
 | 
					// Template returns a fully populated instance of Config.
 | 
				
			||||||
func Template() *Config {
 | 
					func Template() *Config {
 | 
				
			||||||
	return &Config{
 | 
						return &Config{
 | 
				
			||||||
		ID: "org.chromium.Chromium",
 | 
							ID:   "org.chromium.Chromium",
 | 
				
			||||||
		Command: []string{
 | 
							Path: "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
							Args: []string{
 | 
				
			||||||
			"chromium",
 | 
								"chromium",
 | 
				
			||||||
			"--ignore-gpu-blocklist",
 | 
								"--ignore-gpu-blocklist",
 | 
				
			||||||
			"--disable-smooth-scrolling",
 | 
								"--disable-smooth-scrolling",
 | 
				
			||||||
@ -108,11 +99,13 @@ func Template() *Config {
 | 
				
			|||||||
			Inner:    "/var/lib/fortify",
 | 
								Inner:    "/var/lib/fortify",
 | 
				
			||||||
			Sandbox: &SandboxConfig{
 | 
								Sandbox: &SandboxConfig{
 | 
				
			||||||
				Hostname:      "localhost",
 | 
									Hostname:      "localhost",
 | 
				
			||||||
				UserNS:        true,
 | 
									Devel:         true,
 | 
				
			||||||
 | 
									Userns:        true,
 | 
				
			||||||
				Net:           true,
 | 
									Net:           true,
 | 
				
			||||||
				Dev:           true,
 | 
									Dev:           true,
 | 
				
			||||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
									Seccomp:       seccomp.FlagMultiarch,
 | 
				
			||||||
				NoNewSession:  true,
 | 
									Tty:           true,
 | 
				
			||||||
 | 
									Multiarch:     true,
 | 
				
			||||||
				MapRealUID:    true,
 | 
									MapRealUID:    true,
 | 
				
			||||||
				DirectWayland: false,
 | 
									DirectWayland: false,
 | 
				
			||||||
				// example API credentials pulled from Google Chrome
 | 
									// example API credentials pulled from Google Chrome
 | 
				
			||||||
@ -131,10 +124,10 @@ func Template() *Config {
 | 
				
			|||||||
						Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
											Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
				
			||||||
					{Src: "/dev/dri", Device: true},
 | 
										{Src: "/dev/dri", Device: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
									Link:    [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
				
			||||||
				Etc:      "/etc",
 | 
									Etc:     "/etc",
 | 
				
			||||||
				AutoEtc:  true,
 | 
									AutoEtc: true,
 | 
				
			||||||
				Override: []string{"/var/run/nscd"},
 | 
									Cover:   []string{"/var/run/nscd"},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			ExtraPerms: []*ExtraPermConfig{
 | 
								ExtraPerms: []*ExtraPermConfig{
 | 
				
			||||||
				{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
									{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
				
			||||||
@ -160,7 +153,7 @@ func Template() *Config {
 | 
				
			|||||||
				Log:       false,
 | 
									Log:       false,
 | 
				
			||||||
				Filter:    true,
 | 
									Filter:    true,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
								Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							
							
						
						
									
										267
									
								
								fst/sandbox.go
									
									
									
									
									
								
							@ -4,125 +4,149 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxConfig describes resources made available to the sandbox.
 | 
					// SandboxConfig describes resources made available to the sandbox.
 | 
				
			||||||
type SandboxConfig struct {
 | 
					type (
 | 
				
			||||||
	// unix hostname within sandbox
 | 
						SandboxConfig struct {
 | 
				
			||||||
	Hostname string `json:"hostname,omitempty"`
 | 
							// container hostname
 | 
				
			||||||
	// allow userns within sandbox
 | 
							Hostname string `json:"hostname,omitempty"`
 | 
				
			||||||
	UserNS bool `json:"userns,omitempty"`
 | 
					 | 
				
			||||||
	// share net namespace
 | 
					 | 
				
			||||||
	Net bool `json:"net,omitempty"`
 | 
					 | 
				
			||||||
	// share all devices
 | 
					 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
					 | 
				
			||||||
	// seccomp syscall filter policy
 | 
					 | 
				
			||||||
	Syscall *bwrap.SyscallPolicy `json:"syscall"`
 | 
					 | 
				
			||||||
	// do not run in new session
 | 
					 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
					 | 
				
			||||||
	// map target user uid to privileged user uid in the user namespace
 | 
					 | 
				
			||||||
	MapRealUID bool `json:"map_real_uid"`
 | 
					 | 
				
			||||||
	// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
					 | 
				
			||||||
	// and the bare socket is mounted to the sandbox
 | 
					 | 
				
			||||||
	DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// final environment variables
 | 
							// extra seccomp flags
 | 
				
			||||||
	Env map[string]string `json:"env"`
 | 
							Seccomp seccomp.SyscallOpts `json:"seccomp"`
 | 
				
			||||||
	// sandbox host filesystem access
 | 
							// allow ptrace and friends
 | 
				
			||||||
	Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
							Devel bool `json:"devel,omitempty"`
 | 
				
			||||||
	// symlinks created inside the sandbox
 | 
							// allow userns creation in container
 | 
				
			||||||
	Link [][2]string `json:"symlink"`
 | 
							Userns bool `json:"userns,omitempty"`
 | 
				
			||||||
	// read-only /etc directory
 | 
							// share host net namespace
 | 
				
			||||||
	Etc string `json:"etc,omitempty"`
 | 
							Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// automatically set up /etc symlinks
 | 
							// expose main process tty
 | 
				
			||||||
	AutoEtc bool `json:"auto_etc"`
 | 
							Tty bool `json:"tty,omitempty"`
 | 
				
			||||||
	// mount tmpfs over these paths,
 | 
							// allow multiarch
 | 
				
			||||||
	// runs right before [ConfinementConfig.ExtraPerms]
 | 
							Multiarch bool `json:"multiarch,omitempty"`
 | 
				
			||||||
	Override []string `json:"override"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
 | 
							// initial process environment variables
 | 
				
			||||||
type SandboxSys interface {
 | 
							Env map[string]string `json:"env"`
 | 
				
			||||||
	Geteuid() int
 | 
							// map target user uid to privileged user uid in the user namespace
 | 
				
			||||||
	Paths() Paths
 | 
							MapRealUID bool `json:"map_real_uid"`
 | 
				
			||||||
	ReadDir(name string) ([]fs.DirEntry, error)
 | 
					 | 
				
			||||||
	EvalSymlinks(path string) (string, error)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Println(v ...any)
 | 
							// expose all devices
 | 
				
			||||||
	Printf(format string, v ...any)
 | 
							Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
}
 | 
							// container host filesystem bind mounts
 | 
				
			||||||
 | 
							Filesystem []*FilesystemConfig `json:"filesystem"`
 | 
				
			||||||
 | 
							// create symlinks inside container filesystem
 | 
				
			||||||
 | 
							Link [][2]string `json:"symlink"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Bwrap returns the address of the corresponding bwrap.Config to s.
 | 
							// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
 | 
				
			||||||
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
 | 
							// and the bare socket is mounted to the sandbox
 | 
				
			||||||
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
							DirectWayland bool `json:"direct_wayland,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// read-only /etc directory
 | 
				
			||||||
 | 
							Etc string `json:"etc,omitempty"`
 | 
				
			||||||
 | 
							// automatically set up /etc symlinks
 | 
				
			||||||
 | 
							AutoEtc bool `json:"auto_etc"`
 | 
				
			||||||
 | 
							// cover these paths or create them if they do not already exist
 | 
				
			||||||
 | 
							Cover []string `json:"cover"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
 | 
				
			||||||
 | 
						SandboxSys interface {
 | 
				
			||||||
 | 
							Getuid() int
 | 
				
			||||||
 | 
							Getgid() int
 | 
				
			||||||
 | 
							Paths() Paths
 | 
				
			||||||
 | 
							ReadDir(name string) ([]fs.DirEntry, error)
 | 
				
			||||||
 | 
							EvalSymlinks(path string) (string, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Println(v ...any)
 | 
				
			||||||
 | 
							Printf(format string, v ...any)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FilesystemConfig is a representation of [sandbox.BindMount].
 | 
				
			||||||
 | 
						FilesystemConfig struct {
 | 
				
			||||||
 | 
							// mount point in container, same as src if empty
 | 
				
			||||||
 | 
							Dst string `json:"dst,omitempty"`
 | 
				
			||||||
 | 
							// host filesystem path to make available to the container
 | 
				
			||||||
 | 
							Src string `json:"src"`
 | 
				
			||||||
 | 
							// do not mount filesystem read-only
 | 
				
			||||||
 | 
							Write bool `json:"write,omitempty"`
 | 
				
			||||||
 | 
							// do not disable device files
 | 
				
			||||||
 | 
							Device bool `json:"dev,omitempty"`
 | 
				
			||||||
 | 
							// fail if the bind mount cannot be established for any reason
 | 
				
			||||||
 | 
							Must bool `json:"require,omitempty"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToContainer initialises [sandbox.Params] via [SandboxConfig].
 | 
				
			||||||
 | 
					// Note that remaining container setup must be queued by the [App] implementation.
 | 
				
			||||||
 | 
					func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		return nil, errors.New("nil sandbox config")
 | 
							return nil, nil, syscall.EBADE
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.Syscall == nil {
 | 
						container := &sandbox.Params{
 | 
				
			||||||
		sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !s.MapRealUID {
 | 
					 | 
				
			||||||
		// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
 | 
					 | 
				
			||||||
		*uid = 65534
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// some programs fail to connect to dbus session running as a different uid, so a separate workaround
 | 
					 | 
				
			||||||
		// is introduced to map priv-side caller uid in namespace
 | 
					 | 
				
			||||||
		*uid = sys.Geteuid()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conf := (&bwrap.Config{
 | 
					 | 
				
			||||||
		Net:      s.Net,
 | 
					 | 
				
			||||||
		UserNS:   s.UserNS,
 | 
					 | 
				
			||||||
		UID:      uid,
 | 
					 | 
				
			||||||
		GID:      uid,
 | 
					 | 
				
			||||||
		Hostname: s.Hostname,
 | 
							Hostname: s.Hostname,
 | 
				
			||||||
		Clearenv: true,
 | 
							Ops:      new(sandbox.Ops),
 | 
				
			||||||
		SetEnv:   s.Env,
 | 
							Seccomp:  s.Seccomp,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/* this is only 4 KiB of memory on a 64-bit system,
 | 
						/* this is only 4 KiB of memory on a 64-bit system,
 | 
				
			||||||
		permissive defaults on NixOS results in around 100 entries
 | 
						permissive defaults on NixOS results in around 100 entries
 | 
				
			||||||
		so this capacity should eliminate copies for most setups */
 | 
						so this capacity should eliminate copies for most setups */
 | 
				
			||||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
						*container.Ops = slices.Grow(*container.Ops, 1<<8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Syscall:       s.Syscall,
 | 
						if s.Devel {
 | 
				
			||||||
		NewSession:    !s.NoNewSession,
 | 
							container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
		DieWithParent: true,
 | 
						}
 | 
				
			||||||
		AsInit:        true,
 | 
						if s.Userns {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowUserns
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Net {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowNet
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if s.Tty {
 | 
				
			||||||
 | 
							container.Flags |= sandbox.FAllowTTY
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// initialise unconditionally as Once cannot be justified
 | 
						if s.MapRealUID {
 | 
				
			||||||
		// for saving such a miniscule amount of memory
 | 
							/* some programs fail to connect to dbus session running as a different uid
 | 
				
			||||||
		Chmod: make(bwrap.ChmodConfig),
 | 
							so this workaround is introduced to map priv-side caller uid in container */
 | 
				
			||||||
	}).
 | 
							container.Uid = sys.Getuid()
 | 
				
			||||||
		Procfs("/proc").
 | 
							*uid = container.Uid
 | 
				
			||||||
		Tmpfs(Tmp, 4*1024)
 | 
							container.Gid = sys.Getgid()
 | 
				
			||||||
 | 
							*gid = container.Gid
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							*uid = sandbox.OverflowUid()
 | 
				
			||||||
 | 
							*gid = sandbox.OverflowGid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						container.
 | 
				
			||||||
 | 
							Proc("/proc").
 | 
				
			||||||
 | 
							Tmpfs(Tmp, 1<<12, 0755)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.Dev {
 | 
						if !s.Dev {
 | 
				
			||||||
		conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
 | 
							container.Dev("/dev").Mqueue("/dev/mqueue")
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		conf.Bind("/dev", "/dev", false, true, true)
 | 
							container.Bind("/dev", "/dev", sandbox.BindDevice)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !s.AutoEtc {
 | 
						/* retrieve paths and hide them if they're made available in the sandbox;
 | 
				
			||||||
		if s.Etc == "" {
 | 
						this feature tries to improve user experience of permissive defaults, and
 | 
				
			||||||
			conf.Dir("/etc")
 | 
						to warn about issues in custom configuration; it is NOT a security feature
 | 
				
			||||||
		} else {
 | 
						and should not be treated as such, ALWAYS be careful with what you bind */
 | 
				
			||||||
			conf.Bind(s.Etc, "/etc")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// retrieve paths and hide them if they're made available in the sandbox
 | 
					 | 
				
			||||||
	var hidePaths []string
 | 
						var hidePaths []string
 | 
				
			||||||
	sc := sys.Paths()
 | 
						sc := sys.Paths()
 | 
				
			||||||
	hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
						hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
 | 
				
			||||||
	_, systemBusAddr := dbus.Address()
 | 
						_, systemBusAddr := dbus.Address()
 | 
				
			||||||
	if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
						if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// there is usually only one, do not preallocate
 | 
							// there is usually only one, do not preallocate
 | 
				
			||||||
		for _, entry := range entries {
 | 
							for _, entry := range entries {
 | 
				
			||||||
@ -148,7 +172,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
	hidePathMatch := make([]bool, len(hidePaths))
 | 
						hidePathMatch := make([]bool, len(hidePaths))
 | 
				
			||||||
	for i := range hidePaths {
 | 
						for i := range hidePaths {
 | 
				
			||||||
		if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
							if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -158,19 +182,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !path.IsAbs(c.Src) {
 | 
							if !path.IsAbs(c.Src) {
 | 
				
			||||||
			return nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
								return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		dest := c.Dst
 | 
							dest := c.Dst
 | 
				
			||||||
		if c.Dst == "" {
 | 
							if c.Dst == "" {
 | 
				
			||||||
			dest = c.Src
 | 
								dest = c.Src
 | 
				
			||||||
		} else if !path.IsAbs(dest) {
 | 
							} else if !path.IsAbs(dest) {
 | 
				
			||||||
			return nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
								return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		srcH := c.Src
 | 
							srcH := c.Src
 | 
				
			||||||
		if err := evalSymlinks(sys, &srcH); err != nil {
 | 
							if err := evalSymlinks(sys, &srcH); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for i := range hidePaths {
 | 
							for i := range hidePaths {
 | 
				
			||||||
@ -180,54 +204,69 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
								if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
 | 
				
			||||||
				return nil, err
 | 
									return nil, nil, err
 | 
				
			||||||
			} else if ok {
 | 
								} else if ok {
 | 
				
			||||||
				hidePathMatch[i] = true
 | 
									hidePathMatch[i] = true
 | 
				
			||||||
				sys.Printf("hiding paths from %q", c.Src)
 | 
									sys.Printf("hiding paths from %q", c.Src)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
 | 
							var flags int
 | 
				
			||||||
 | 
							if c.Write {
 | 
				
			||||||
 | 
								flags |= sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.Device {
 | 
				
			||||||
 | 
								flags |= sandbox.BindDevice | sandbox.BindWritable
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !c.Must {
 | 
				
			||||||
 | 
								flags |= sandbox.BindOptional
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.Bind(c.Src, dest, flags)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// hide marked paths before setting up shares
 | 
						// cover matched paths
 | 
				
			||||||
	for i, ok := range hidePathMatch {
 | 
						for i, ok := range hidePathMatch {
 | 
				
			||||||
		if ok {
 | 
							if ok {
 | 
				
			||||||
			conf.Tmpfs(hidePaths[i], 8192)
 | 
								container.Tmpfs(hidePaths[i], 1<<13, 0755)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, l := range s.Link {
 | 
						for _, l := range s.Link {
 | 
				
			||||||
		conf.Symlink(l[0], l[1])
 | 
							container.Link(l[0], l[1])
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.AutoEtc {
 | 
						// perf: this might work better if implemented as a setup op in container init
 | 
				
			||||||
		etc := s.Etc
 | 
						if !s.AutoEtc {
 | 
				
			||||||
		if etc == "" {
 | 
							if s.Etc != "" {
 | 
				
			||||||
			etc = "/etc"
 | 
								container.Bind(s.Etc, "/etc", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		conf.Bind(etc, Tmp+"/etc")
 | 
						} else {
 | 
				
			||||||
 | 
							etcPath := s.Etc
 | 
				
			||||||
 | 
							if etcPath == "" {
 | 
				
			||||||
 | 
								etcPath = "/etc"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							container.Bind(etcPath, Tmp+"/etc", 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// link host /etc contents to prevent passwd/group from being overwritten
 | 
							// link host /etc contents to prevent dropping passwd/group bind mounts
 | 
				
			||||||
		if d, err := sys.ReadDir(etc); err != nil {
 | 
							if d, err := sys.ReadDir(etcPath); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			for _, ent := range d {
 | 
								for _, ent := range d {
 | 
				
			||||||
				name := ent.Name()
 | 
									n := ent.Name()
 | 
				
			||||||
				switch name {
 | 
									switch n {
 | 
				
			||||||
				case "passwd":
 | 
									case "passwd":
 | 
				
			||||||
				case "group":
 | 
									case "group":
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case "mtab":
 | 
									case "mtab":
 | 
				
			||||||
					conf.Symlink("/proc/mounts", "/etc/"+name)
 | 
										container.Link("/proc/mounts", "/etc/"+n)
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
					conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
 | 
										container.Link(Tmp+"/etc/"+n, "/etc/"+n)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return conf, nil
 | 
						return container, maps.Clone(s.Env), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func evalSymlinks(sys SandboxSys, v *string) error {
 | 
					func evalSymlinks(sys SandboxSys, v *string) error {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -1,3 +1,3 @@
 | 
				
			|||||||
module git.gensokyo.uk/security/fortify
 | 
					module git.gensokyo.uk/security/fortify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.22
 | 
					go 1.23
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Test_argsFD_String(t *testing.T) {
 | 
					func Test_argsFd_String(t *testing.T) {
 | 
				
			||||||
	wantString := strings.Join(wantArgs, " ")
 | 
						wantString := strings.Join(wantArgs, " ")
 | 
				
			||||||
	if got := argsWt.(fmt.Stringer).String(); got != wantString {
 | 
						if got := argsWt.(fmt.Stringer).String(); got != wantString {
 | 
				
			||||||
		t.Errorf("String(): got %v; want %v",
 | 
							t.Errorf("String(): got %v; want %v",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,96 +0,0 @@
 | 
				
			|||||||
package helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// BubblewrapName is the file name or path to bubblewrap.
 | 
					 | 
				
			||||||
var BubblewrapName = "bwrap"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type bubblewrap struct {
 | 
					 | 
				
			||||||
	// final args fd of bwrap process
 | 
					 | 
				
			||||||
	argsFd uintptr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// name of the command to run in bwrap
 | 
					 | 
				
			||||||
	name string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// whether to set process group id
 | 
					 | 
				
			||||||
	setpgid bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	lock sync.RWMutex
 | 
					 | 
				
			||||||
	*helperCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
 | 
					 | 
				
			||||||
	b.lock.Lock()
 | 
					 | 
				
			||||||
	defer b.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
					 | 
				
			||||||
	// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
					 | 
				
			||||||
	if b.Cmd != nil && b.Cmd.Process != nil {
 | 
					 | 
				
			||||||
		return errors.New("exec: already started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args := b.finalise(ctx, stat)
 | 
					 | 
				
			||||||
	if b.setpgid {
 | 
					 | 
				
			||||||
		b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
 | 
					 | 
				
			||||||
	b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
 | 
					 | 
				
			||||||
	b.Cmd.Args = append(b.Cmd.Args, args...)
 | 
					 | 
				
			||||||
	return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
					 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					 | 
				
			||||||
func MustNewBwrap(
 | 
					 | 
				
			||||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
					 | 
				
			||||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
					 | 
				
			||||||
	extraFiles []*os.File,
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) Helper {
 | 
					 | 
				
			||||||
	b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		panic(err.Error())
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return b
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
					 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					 | 
				
			||||||
func NewBwrap(
 | 
					 | 
				
			||||||
	conf *bwrap.Config, name string, setpgid bool,
 | 
					 | 
				
			||||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
					 | 
				
			||||||
	extraFiles []*os.File,
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) (Helper, error) {
 | 
					 | 
				
			||||||
	b := new(bubblewrap)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	b.name = name
 | 
					 | 
				
			||||||
	b.setpgid = setpgid
 | 
					 | 
				
			||||||
	b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		f := proc.NewWriterTo(v)
 | 
					 | 
				
			||||||
		b.argsFd = proc.InitFile(f, b.extraFiles)
 | 
					 | 
				
			||||||
		b.files = append(b.files, f)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return b, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,72 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Builder interface {
 | 
					 | 
				
			||||||
	Len() int
 | 
					 | 
				
			||||||
	Append(args *[]string)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type FSBuilder interface {
 | 
					 | 
				
			||||||
	Path() string
 | 
					 | 
				
			||||||
	Builder
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type FDBuilder interface {
 | 
					 | 
				
			||||||
	proc.File
 | 
					 | 
				
			||||||
	Builder
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Args returns a slice of bwrap args corresponding to c.
 | 
					 | 
				
			||||||
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
 | 
					 | 
				
			||||||
	builders := []Builder{
 | 
					 | 
				
			||||||
		c.boolArgs(),
 | 
					 | 
				
			||||||
		c.intArgs(),
 | 
					 | 
				
			||||||
		c.stringArgs(),
 | 
					 | 
				
			||||||
		c.pairArgs(),
 | 
					 | 
				
			||||||
		c.seccompArgs(),
 | 
					 | 
				
			||||||
		newFile(SyncFd.String(), syncFd),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	builders = slices.Grow(builders, len(c.Filesystem)+1)
 | 
					 | 
				
			||||||
	for _, f := range c.Filesystem {
 | 
					 | 
				
			||||||
		builders = append(builders, f)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	builders = append(builders, c.Chmod)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	argc := 0
 | 
					 | 
				
			||||||
	fc := 0
 | 
					 | 
				
			||||||
	for _, b := range builders {
 | 
					 | 
				
			||||||
		l := b.Len()
 | 
					 | 
				
			||||||
		if l < 1 {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		argc += l
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if f, ok := b.(FDBuilder); ok {
 | 
					 | 
				
			||||||
			fc++
 | 
					 | 
				
			||||||
			proc.InitFile(f, extraFiles)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fc++ // allocate extra slot for stat fd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args = make([]string, 0, argc)
 | 
					 | 
				
			||||||
	*files = slices.Grow(*files, fc)
 | 
					 | 
				
			||||||
	for _, b := range builders {
 | 
					 | 
				
			||||||
		if b.Len() < 1 {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		b.Append(&args)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if f, ok := b.(FDBuilder); ok {
 | 
					 | 
				
			||||||
			*files = append(*files, f)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,199 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
Bind binds mount src on host to dest in sandbox.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest) bind mount host path readonly on sandbox
 | 
					 | 
				
			||||||
(--ro-bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--ro-bind-try SRC DEST).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest, false, true) bind mount host path on sandbox.
 | 
					 | 
				
			||||||
(--bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--bind-try SRC DEST).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
 | 
					 | 
				
			||||||
(--dev-bind SRC DEST).
 | 
					 | 
				
			||||||
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
 | 
					 | 
				
			||||||
(--dev-bind-try SRC DEST).
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		try   bool
 | 
					 | 
				
			||||||
		write bool
 | 
					 | 
				
			||||||
		dev   bool
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(opts) > 0 {
 | 
					 | 
				
			||||||
		try = opts[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(opts) > 1 {
 | 
					 | 
				
			||||||
		write = opts[1]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(opts) > 2 {
 | 
					 | 
				
			||||||
		dev = opts[2]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if dev {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	} else if write {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		if try {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// WriteFile copy from FD to destination DEST
 | 
					 | 
				
			||||||
// (--file FD DEST)
 | 
					 | 
				
			||||||
func (c *Config) WriteFile(name string, data []byte) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, Type: DataWrite})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
CopyBind copy from FD to file which is readonly bind-mounted on DEST
 | 
					 | 
				
			||||||
(--ro-bind-data FD DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
 | 
					 | 
				
			||||||
(--bind-data FD DEST)
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	var p *[]byte
 | 
					 | 
				
			||||||
	c.CopyBindRef(dest, &p, opts...)
 | 
					 | 
				
			||||||
	*p = payload
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data.
 | 
					 | 
				
			||||||
func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config {
 | 
					 | 
				
			||||||
	t := DataROBind
 | 
					 | 
				
			||||||
	if len(opts) > 0 && opts[0] {
 | 
					 | 
				
			||||||
		t = DataBind
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	d := &DataConfig{Dest: dest, Type: t}
 | 
					 | 
				
			||||||
	*payloadRef = &d.Data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, d)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Dir create dir in sandbox
 | 
					 | 
				
			||||||
// (--dir DEST)
 | 
					 | 
				
			||||||
func (c *Config) Dir(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// RemountRO remount path as readonly; does not recursively remount
 | 
					 | 
				
			||||||
// (--remount-ro DEST)
 | 
					 | 
				
			||||||
func (c *Config) RemountRO(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Procfs mount new procfs in sandbox
 | 
					 | 
				
			||||||
// (--proc DEST)
 | 
					 | 
				
			||||||
func (c *Config) Procfs(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DevTmpfs mount new dev in sandbox
 | 
					 | 
				
			||||||
// (--dev DEST)
 | 
					 | 
				
			||||||
func (c *Config) DevTmpfs(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Mqueue mount new mqueue in sandbox
 | 
					 | 
				
			||||||
// (--mqueue DEST)
 | 
					 | 
				
			||||||
func (c *Config) Mqueue(dest string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Tmpfs mount new tmpfs in sandbox
 | 
					 | 
				
			||||||
// (--tmpfs DEST)
 | 
					 | 
				
			||||||
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
 | 
					 | 
				
			||||||
	tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
 | 
					 | 
				
			||||||
	if size >= 0 {
 | 
					 | 
				
			||||||
		tmpfs.Inner.Size = size
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(perm) == 1 {
 | 
					 | 
				
			||||||
		tmpfs.Mode = &perm[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, tmpfs)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
 | 
					 | 
				
			||||||
// (--tmp-overlay DEST)
 | 
					 | 
				
			||||||
func (c *Config) Overlay(dest string, src ...string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Join mount overlayfs read-only on DEST
 | 
					 | 
				
			||||||
// (--ro-overlay DEST)
 | 
					 | 
				
			||||||
func (c *Config) Join(dest string, src ...string) *Config {
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
 | 
					 | 
				
			||||||
// WORKDIR an empty directory on the same filesystem as RWSRC
 | 
					 | 
				
			||||||
// (--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
 | 
					 | 
				
			||||||
	if rwsrc == "" || workdir == "" {
 | 
					 | 
				
			||||||
		panic("persist called without required paths")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Symlink create symlink within sandbox
 | 
					 | 
				
			||||||
// (--symlink SRC DEST)
 | 
					 | 
				
			||||||
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
 | 
					 | 
				
			||||||
	symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
 | 
					 | 
				
			||||||
	if len(perm) == 1 {
 | 
					 | 
				
			||||||
		symlink.Mode = &perm[0]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Filesystem = append(c.Filesystem, symlink)
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
 | 
					 | 
				
			||||||
func (c *Config) SetUID(uid int) *Config {
 | 
					 | 
				
			||||||
	if uid >= 0 {
 | 
					 | 
				
			||||||
		c.UID = &uid
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
 | 
					 | 
				
			||||||
func (c *Config) SetGID(gid int) *Config {
 | 
					 | 
				
			||||||
	if gid >= 0 {
 | 
					 | 
				
			||||||
		c.GID = &gid
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return c
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,104 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Config struct {
 | 
					 | 
				
			||||||
	// unshare every namespace we support by default if nil
 | 
					 | 
				
			||||||
	// (--unshare-all)
 | 
					 | 
				
			||||||
	Unshare *UnshareConfig `json:"unshare,omitempty"`
 | 
					 | 
				
			||||||
	// retain the network namespace (can only combine with nil Unshare)
 | 
					 | 
				
			||||||
	// (--share-net)
 | 
					 | 
				
			||||||
	Net bool `json:"net"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// disable further use of user namespaces inside sandbox and fail unless
 | 
					 | 
				
			||||||
	// further use of user namespace inside sandbox is disabled if false
 | 
					 | 
				
			||||||
	// (--disable-userns) (--assert-userns-disabled)
 | 
					 | 
				
			||||||
	UserNS bool `json:"userns"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// custom uid in the sandbox, requires new user namespace
 | 
					 | 
				
			||||||
	// (--uid UID)
 | 
					 | 
				
			||||||
	UID *int `json:"uid,omitempty"`
 | 
					 | 
				
			||||||
	// custom gid in the sandbox, requires new user namespace
 | 
					 | 
				
			||||||
	// (--gid GID)
 | 
					 | 
				
			||||||
	GID *int `json:"gid,omitempty"`
 | 
					 | 
				
			||||||
	// custom hostname in the sandbox, requires new uts namespace
 | 
					 | 
				
			||||||
	// (--hostname NAME)
 | 
					 | 
				
			||||||
	Hostname string `json:"hostname,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// change directory
 | 
					 | 
				
			||||||
	// (--chdir DIR)
 | 
					 | 
				
			||||||
	Chdir string `json:"chdir,omitempty"`
 | 
					 | 
				
			||||||
	// unset all environment variables
 | 
					 | 
				
			||||||
	// (--clearenv)
 | 
					 | 
				
			||||||
	Clearenv bool `json:"clearenv"`
 | 
					 | 
				
			||||||
	// set environment variable
 | 
					 | 
				
			||||||
	// (--setenv VAR VALUE)
 | 
					 | 
				
			||||||
	SetEnv map[string]string `json:"setenv,omitempty"`
 | 
					 | 
				
			||||||
	// unset environment variables
 | 
					 | 
				
			||||||
	// (--unsetenv VAR)
 | 
					 | 
				
			||||||
	UnsetEnv []string `json:"unsetenv,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// take a lock on file while sandbox is running
 | 
					 | 
				
			||||||
	// (--lock-file DEST)
 | 
					 | 
				
			||||||
	LockFile []string `json:"lock_file,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ordered filesystem args
 | 
					 | 
				
			||||||
	Filesystem []FSBuilder `json:"filesystem,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// change permissions (must already exist)
 | 
					 | 
				
			||||||
	// (--chmod OCTAL PATH)
 | 
					 | 
				
			||||||
	Chmod ChmodConfig `json:"chmod,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// load and use seccomp rules from FD (not repeatable)
 | 
					 | 
				
			||||||
	// (--seccomp FD)
 | 
					 | 
				
			||||||
	Syscall *SyscallPolicy
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// create a new terminal session
 | 
					 | 
				
			||||||
	// (--new-session)
 | 
					 | 
				
			||||||
	NewSession bool `json:"new_session"`
 | 
					 | 
				
			||||||
	// kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
 | 
					 | 
				
			||||||
	// (--die-with-parent)
 | 
					 | 
				
			||||||
	DieWithParent bool `json:"die_with_parent"`
 | 
					 | 
				
			||||||
	// do not install a reaper process with PID=1
 | 
					 | 
				
			||||||
	// (--as-pid-1)
 | 
					 | 
				
			||||||
	AsInit bool `json:"as_init"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/* unmapped options include:
 | 
					 | 
				
			||||||
	    --unshare-user-try           Create new user namespace if possible else continue by skipping it
 | 
					 | 
				
			||||||
	    --unshare-cgroup-try         Create new cgroup namespace if possible else continue by skipping it
 | 
					 | 
				
			||||||
	    --userns FD                  Use this user namespace (cannot combine with --unshare-user)
 | 
					 | 
				
			||||||
	    --userns2 FD                 After setup switch to this user namespace, only useful with --userns
 | 
					 | 
				
			||||||
	    --pidns FD                   Use this pid namespace (as parent namespace if using --unshare-pid)
 | 
					 | 
				
			||||||
	    --bind-fd FD DEST            Bind open directory or path fd on DEST
 | 
					 | 
				
			||||||
	    --ro-bind-fd FD DEST         Bind open directory or path fd read-only on DEST
 | 
					 | 
				
			||||||
	    --exec-label LABEL           Exec label for the sandbox
 | 
					 | 
				
			||||||
	    --file-label LABEL           File label for temporary sandbox content
 | 
					 | 
				
			||||||
	    --add-seccomp-fd FD          Load and use seccomp rules from FD (repeatable)
 | 
					 | 
				
			||||||
	    --block-fd FD                Block on FD until some data to read is available
 | 
					 | 
				
			||||||
	    --userns-block-fd FD         Block on FD until the user namespace is ready
 | 
					 | 
				
			||||||
	    --info-fd FD                 Write information about the running container to FD
 | 
					 | 
				
			||||||
	    --json-status-fd FD          Write container status to FD as multiple JSON documents
 | 
					 | 
				
			||||||
	    --cap-add CAP                Add cap CAP when running as privileged user
 | 
					 | 
				
			||||||
	    --cap-drop CAP               Drop cap CAP when running as privileged user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	among which --args is used internally for passing arguments */
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type UnshareConfig struct {
 | 
					 | 
				
			||||||
	// (--unshare-user)
 | 
					 | 
				
			||||||
	// create new user namespace
 | 
					 | 
				
			||||||
	User bool `json:"user"`
 | 
					 | 
				
			||||||
	// (--unshare-ipc)
 | 
					 | 
				
			||||||
	// create new ipc namespace
 | 
					 | 
				
			||||||
	IPC bool `json:"ipc"`
 | 
					 | 
				
			||||||
	// (--unshare-pid)
 | 
					 | 
				
			||||||
	// create new pid namespace
 | 
					 | 
				
			||||||
	PID bool `json:"pid"`
 | 
					 | 
				
			||||||
	// (--unshare-net)
 | 
					 | 
				
			||||||
	// create new network namespace
 | 
					 | 
				
			||||||
	Net bool `json:"net"`
 | 
					 | 
				
			||||||
	// (--unshare-uts)
 | 
					 | 
				
			||||||
	// create new uts namespace
 | 
					 | 
				
			||||||
	UTS bool `json:"uts"`
 | 
					 | 
				
			||||||
	// (--unshare-cgroup)
 | 
					 | 
				
			||||||
	// create new cgroup namespace
 | 
					 | 
				
			||||||
	CGroup bool `json:"cgroup"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,257 +0,0 @@
 | 
				
			|||||||
package bwrap_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestConfig_Args(t *testing.T) {
 | 
					 | 
				
			||||||
	seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
	t.Cleanup(func() { seccomp.CPrintln = nil })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	testCases := []struct {
 | 
					 | 
				
			||||||
		name string
 | 
					 | 
				
			||||||
		conf *bwrap.Config
 | 
					 | 
				
			||||||
		want []string
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"bind", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Bind("/etc", "/.fortify/etc").
 | 
					 | 
				
			||||||
				Bind("/etc", "/.fortify/etc", true).
 | 
					 | 
				
			||||||
				Bind("/run", "/.fortify/run", false, true).
 | 
					 | 
				
			||||||
				Bind("/sys/devices", "/.fortify/sys/devices", true, true).
 | 
					 | 
				
			||||||
				Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
 | 
					 | 
				
			||||||
				Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Bind("/etc", "/.fortify/etc")
 | 
					 | 
				
			||||||
				"--ro-bind", "/etc", "/.fortify/etc",
 | 
					 | 
				
			||||||
				// Bind("/etc", "/.fortify/etc", true)
 | 
					 | 
				
			||||||
				"--ro-bind-try", "/etc", "/.fortify/etc",
 | 
					 | 
				
			||||||
				// Bind("/run", "/.fortify/run", false, true)
 | 
					 | 
				
			||||||
				"--bind", "/run", "/.fortify/run",
 | 
					 | 
				
			||||||
				// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
 | 
					 | 
				
			||||||
				"--bind-try", "/sys/devices", "/.fortify/sys/devices",
 | 
					 | 
				
			||||||
				// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
 | 
					 | 
				
			||||||
				"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
 | 
					 | 
				
			||||||
				// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
 | 
					 | 
				
			||||||
				"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Dir("/.fortify").
 | 
					 | 
				
			||||||
				RemountRO("/home").
 | 
					 | 
				
			||||||
				Procfs("/proc").
 | 
					 | 
				
			||||||
				DevTmpfs("/dev").
 | 
					 | 
				
			||||||
				Mqueue("/dev/mqueue"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Dir("/.fortify")
 | 
					 | 
				
			||||||
				"--dir", "/.fortify",
 | 
					 | 
				
			||||||
				// RemountRO("/home")
 | 
					 | 
				
			||||||
				"--remount-ro", "/home",
 | 
					 | 
				
			||||||
				// Procfs("/proc")
 | 
					 | 
				
			||||||
				"--proc", "/proc",
 | 
					 | 
				
			||||||
				// DevTmpfs("/dev")
 | 
					 | 
				
			||||||
				"--dev", "/dev",
 | 
					 | 
				
			||||||
				// Mqueue("/dev/mqueue")
 | 
					 | 
				
			||||||
				"--mqueue", "/dev/mqueue",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"tmpfs", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Tmpfs("/run/user", 8192).
 | 
					 | 
				
			||||||
				Tmpfs("/run/dbus", 8192, 0755),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Tmpfs("/run/user", 8192)
 | 
					 | 
				
			||||||
				"--size", "8192", "--tmpfs", "/run/user",
 | 
					 | 
				
			||||||
				// Tmpfs("/run/dbus", 8192, 0755)
 | 
					 | 
				
			||||||
				"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"symlink", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Symlink("/.fortify/sbin/init", "/sbin/init").
 | 
					 | 
				
			||||||
				Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Symlink("/.fortify/sbin/init", "/sbin/init")
 | 
					 | 
				
			||||||
				"--symlink", "/.fortify/sbin/init", "/sbin/init",
 | 
					 | 
				
			||||||
				// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
 | 
					 | 
				
			||||||
				"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"overlayfs", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				Overlay("/etc", "/etc").
 | 
					 | 
				
			||||||
				Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
 | 
					 | 
				
			||||||
				Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Overlay("/etc", "/etc")
 | 
					 | 
				
			||||||
				"--overlay-src", "/etc", "--tmp-overlay", "/etc",
 | 
					 | 
				
			||||||
				// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
 | 
					 | 
				
			||||||
				"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
 | 
					 | 
				
			||||||
				"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
 | 
					 | 
				
			||||||
				// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
 | 
					 | 
				
			||||||
				"--overlay-src", "/data/app/org.chromium.Chromium/nix",
 | 
					 | 
				
			||||||
				"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"copy", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				WriteFile("/.fortify/version", make([]byte, 8)).
 | 
					 | 
				
			||||||
				CopyBind("/etc/group", make([]byte, 8)).
 | 
					 | 
				
			||||||
				CopyBind("/etc/passwd", make([]byte, 8), true),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Write("/.fortify/version", make([]byte, 8))
 | 
					 | 
				
			||||||
				"--file", "3", "/.fortify/version",
 | 
					 | 
				
			||||||
				// CopyBind("/etc/group", make([]byte, 8))
 | 
					 | 
				
			||||||
				"--ro-bind-data", "4", "/etc/group",
 | 
					 | 
				
			||||||
				// CopyBind("/etc/passwd", make([]byte, 8), true)
 | 
					 | 
				
			||||||
				"--bind-data", "5", "/etc/passwd",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
 | 
					 | 
				
			||||||
				User:   false,
 | 
					 | 
				
			||||||
				IPC:    false,
 | 
					 | 
				
			||||||
				PID:    false,
 | 
					 | 
				
			||||||
				Net:    false,
 | 
					 | 
				
			||||||
				UTS:    false,
 | 
					 | 
				
			||||||
				CGroup: false,
 | 
					 | 
				
			||||||
			}},
 | 
					 | 
				
			||||||
			[]string{"--disable-userns", "--assert-userns-disabled"},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"uid gid sync", (new(bwrap.Config)).
 | 
					 | 
				
			||||||
				SetUID(1971).
 | 
					 | 
				
			||||||
				SetGID(100),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// SetUID(1971)
 | 
					 | 
				
			||||||
				"--uid", "1971",
 | 
					 | 
				
			||||||
				// SetGID(100)
 | 
					 | 
				
			||||||
				"--gid", "100",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
 | 
					 | 
				
			||||||
				Hostname: "fortify",
 | 
					 | 
				
			||||||
				Chdir:    "/.fortify",
 | 
					 | 
				
			||||||
				SetEnv:   map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
 | 
					 | 
				
			||||||
				UnsetEnv: []string{"HOME", "HOST"},
 | 
					 | 
				
			||||||
				LockFile: []string{"/.fortify/lock"},
 | 
					 | 
				
			||||||
				Syscall:  new(bwrap.SyscallPolicy),
 | 
					 | 
				
			||||||
				Chmod:    map[string]os.FileMode{"/.fortify/sbin/init": 0755},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				// Hostname: "fortify"
 | 
					 | 
				
			||||||
				"--hostname", "fortify",
 | 
					 | 
				
			||||||
				// Chdir: "/.fortify"
 | 
					 | 
				
			||||||
				"--chdir", "/.fortify",
 | 
					 | 
				
			||||||
				// UnsetEnv: []string{"HOME", "HOST"}
 | 
					 | 
				
			||||||
				"--unsetenv", "HOME",
 | 
					 | 
				
			||||||
				"--unsetenv", "HOST",
 | 
					 | 
				
			||||||
				// LockFile: []string{"/.fortify/lock"},
 | 
					 | 
				
			||||||
				"--lock-file", "/.fortify/lock",
 | 
					 | 
				
			||||||
				// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
 | 
					 | 
				
			||||||
				"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
 | 
					 | 
				
			||||||
				// Syscall: new(bwrap.SyscallPolicy),
 | 
					 | 
				
			||||||
				"--seccomp", "3",
 | 
					 | 
				
			||||||
				// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
 | 
					 | 
				
			||||||
				"--chmod", "755", "/.fortify/sbin/init",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
 | 
					 | 
				
			||||||
				Symlink("usr/bin", "/bin").
 | 
					 | 
				
			||||||
				Symlink("var/home", "/home").
 | 
					 | 
				
			||||||
				Symlink("usr/lib", "/lib").
 | 
					 | 
				
			||||||
				Symlink("usr/lib64", "/lib64").
 | 
					 | 
				
			||||||
				Symlink("run/media", "/media").
 | 
					 | 
				
			||||||
				Symlink("var/mnt", "/mnt").
 | 
					 | 
				
			||||||
				Symlink("var/opt", "/opt").
 | 
					 | 
				
			||||||
				Symlink("sysroot/ostree", "/ostree").
 | 
					 | 
				
			||||||
				Symlink("var/roothome", "/root").
 | 
					 | 
				
			||||||
				Symlink("usr/sbin", "/sbin").
 | 
					 | 
				
			||||||
				Symlink("var/srv", "/srv").
 | 
					 | 
				
			||||||
				Bind("/run", "/run", false, true).
 | 
					 | 
				
			||||||
				Bind("/tmp", "/tmp", false, true).
 | 
					 | 
				
			||||||
				Bind("/var", "/var", false, true).
 | 
					 | 
				
			||||||
				Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
 | 
					 | 
				
			||||||
				Bind("/boot", "/boot").
 | 
					 | 
				
			||||||
				Bind("/dev", "/dev").
 | 
					 | 
				
			||||||
				Bind("/proc", "/proc").
 | 
					 | 
				
			||||||
				Bind("/sys", "/sys").
 | 
					 | 
				
			||||||
				Bind("/sysroot", "/sysroot").
 | 
					 | 
				
			||||||
				Bind("/usr", "/usr").
 | 
					 | 
				
			||||||
				Bind("/etc", "/etc"),
 | 
					 | 
				
			||||||
			[]string{
 | 
					 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
					 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
					 | 
				
			||||||
				"--clearenv", "--die-with-parent",
 | 
					 | 
				
			||||||
				"--symlink", "usr/bin", "/bin",
 | 
					 | 
				
			||||||
				"--symlink", "var/home", "/home",
 | 
					 | 
				
			||||||
				"--symlink", "usr/lib", "/lib",
 | 
					 | 
				
			||||||
				"--symlink", "usr/lib64", "/lib64",
 | 
					 | 
				
			||||||
				"--symlink", "run/media", "/media",
 | 
					 | 
				
			||||||
				"--symlink", "var/mnt", "/mnt",
 | 
					 | 
				
			||||||
				"--symlink", "var/opt", "/opt",
 | 
					 | 
				
			||||||
				"--symlink", "sysroot/ostree", "/ostree",
 | 
					 | 
				
			||||||
				"--symlink", "var/roothome", "/root",
 | 
					 | 
				
			||||||
				"--symlink", "usr/sbin", "/sbin",
 | 
					 | 
				
			||||||
				"--symlink", "var/srv", "/srv",
 | 
					 | 
				
			||||||
				"--bind", "/run", "/run",
 | 
					 | 
				
			||||||
				"--bind", "/tmp", "/tmp",
 | 
					 | 
				
			||||||
				"--bind", "/var", "/var",
 | 
					 | 
				
			||||||
				"--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
 | 
					 | 
				
			||||||
				"--ro-bind", "/boot", "/boot",
 | 
					 | 
				
			||||||
				"--ro-bind", "/dev", "/dev",
 | 
					 | 
				
			||||||
				"--ro-bind", "/proc", "/proc",
 | 
					 | 
				
			||||||
				"--ro-bind", "/sys", "/sys",
 | 
					 | 
				
			||||||
				"--ro-bind", "/sysroot", "/sysroot",
 | 
					 | 
				
			||||||
				"--ro-bind", "/usr", "/usr",
 | 
					 | 
				
			||||||
				"--ro-bind", "/etc", "/etc",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, tc := range testCases {
 | 
					 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
					 | 
				
			||||||
			if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
 | 
					 | 
				
			||||||
				t.Errorf("Args() = %#v, want %#v", got, tc.want)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// test persist validation
 | 
					 | 
				
			||||||
	t.Run("invalid persist", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer func() {
 | 
					 | 
				
			||||||
			wantPanic := "persist called without required paths"
 | 
					 | 
				
			||||||
			if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
				t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
		(new(bwrap.Config)).Persist("/run", "", "")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,85 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SyscallPolicy struct {
 | 
					 | 
				
			||||||
	// disable fortify extensions
 | 
					 | 
				
			||||||
	Compat bool `json:"compat"`
 | 
					 | 
				
			||||||
	// deny development syscalls
 | 
					 | 
				
			||||||
	DenyDevel bool `json:"deny_devel"`
 | 
					 | 
				
			||||||
	// deny multiarch/emulation syscalls
 | 
					 | 
				
			||||||
	Multiarch bool `json:"multiarch"`
 | 
					 | 
				
			||||||
	// allow PER_LINUX32
 | 
					 | 
				
			||||||
	Linux32 bool `json:"linux32"`
 | 
					 | 
				
			||||||
	// allow AF_CAN
 | 
					 | 
				
			||||||
	Can bool `json:"can"`
 | 
					 | 
				
			||||||
	// allow AF_BLUETOOTH
 | 
					 | 
				
			||||||
	Bluetooth bool `json:"bluetooth"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) seccompArgs() FDBuilder {
 | 
					 | 
				
			||||||
	// explicitly disable syscall filter
 | 
					 | 
				
			||||||
	if c.Syscall == nil {
 | 
					 | 
				
			||||||
		// nil File skips builder
 | 
					 | 
				
			||||||
		return new(seccompBuilder)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		opts    seccomp.SyscallOpts
 | 
					 | 
				
			||||||
		optd    []string
 | 
					 | 
				
			||||||
		optCond = [...]struct {
 | 
					 | 
				
			||||||
			v bool
 | 
					 | 
				
			||||||
			o seccomp.SyscallOpts
 | 
					 | 
				
			||||||
			d string
 | 
					 | 
				
			||||||
		}{
 | 
					 | 
				
			||||||
			{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
 | 
					 | 
				
			||||||
			{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
 | 
					 | 
				
			||||||
			{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
 | 
					 | 
				
			||||||
			{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
 | 
					 | 
				
			||||||
			{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
 | 
					 | 
				
			||||||
			{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
 | 
					 | 
				
			||||||
			{c.Syscall.Can, seccomp.FlagCan, "can"},
 | 
					 | 
				
			||||||
			{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
		optd = make([]string, 1, len(optCond)+1)
 | 
					 | 
				
			||||||
		optd[0] = "common"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	for _, opt := range optCond {
 | 
					 | 
				
			||||||
		if opt.v {
 | 
					 | 
				
			||||||
			opts |= opt.o
 | 
					 | 
				
			||||||
			if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
				optd = append(optd, opt.d)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if seccomp.CPrintln != nil {
 | 
					 | 
				
			||||||
		seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &seccompBuilder{seccomp.NewFile(opts)}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type seccompBuilder struct{ proc.File }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *seccompBuilder) Len() int {
 | 
					 | 
				
			||||||
	if s == nil || s.File == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *seccompBuilder) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if s == nil || s.File == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,273 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	gob.Register(new(PermConfig[SymlinkConfig]))
 | 
					 | 
				
			||||||
	gob.Register(new(PermConfig[*TmpfsConfig]))
 | 
					 | 
				
			||||||
	gob.Register(new(OverlayConfig))
 | 
					 | 
				
			||||||
	gob.Register(new(DataConfig))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PositionalArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p PositionalArg) String() string { return positionalArgs[p] }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	Tmpfs PositionalArg = iota
 | 
					 | 
				
			||||||
	Symlink
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Bind
 | 
					 | 
				
			||||||
	BindTry
 | 
					 | 
				
			||||||
	DevBind
 | 
					 | 
				
			||||||
	DevBindTry
 | 
					 | 
				
			||||||
	ROBind
 | 
					 | 
				
			||||||
	ROBindTry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Chmod
 | 
					 | 
				
			||||||
	Dir
 | 
					 | 
				
			||||||
	RemountRO
 | 
					 | 
				
			||||||
	Procfs
 | 
					 | 
				
			||||||
	DevTmpfs
 | 
					 | 
				
			||||||
	Mqueue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Perms
 | 
					 | 
				
			||||||
	Size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	OverlaySrc
 | 
					 | 
				
			||||||
	Overlay
 | 
					 | 
				
			||||||
	TmpOverlay
 | 
					 | 
				
			||||||
	ROOverlay
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	SyncFd
 | 
					 | 
				
			||||||
	Seccomp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	File
 | 
					 | 
				
			||||||
	BindData
 | 
					 | 
				
			||||||
	ROBindData
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var positionalArgs = [...]string{
 | 
					 | 
				
			||||||
	Tmpfs:   "--tmpfs",
 | 
					 | 
				
			||||||
	Symlink: "--symlink",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Bind:       "--bind",
 | 
					 | 
				
			||||||
	BindTry:    "--bind-try",
 | 
					 | 
				
			||||||
	DevBind:    "--dev-bind",
 | 
					 | 
				
			||||||
	DevBindTry: "--dev-bind-try",
 | 
					 | 
				
			||||||
	ROBind:     "--ro-bind",
 | 
					 | 
				
			||||||
	ROBindTry:  "--ro-bind-try",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Chmod:     "--chmod",
 | 
					 | 
				
			||||||
	Dir:       "--dir",
 | 
					 | 
				
			||||||
	RemountRO: "--remount-ro",
 | 
					 | 
				
			||||||
	Procfs:    "--proc",
 | 
					 | 
				
			||||||
	DevTmpfs:  "--dev",
 | 
					 | 
				
			||||||
	Mqueue:    "--mqueue",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Perms: "--perms",
 | 
					 | 
				
			||||||
	Size:  "--size",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	OverlaySrc: "--overlay-src",
 | 
					 | 
				
			||||||
	Overlay:    "--overlay",
 | 
					 | 
				
			||||||
	TmpOverlay: "--tmp-overlay",
 | 
					 | 
				
			||||||
	ROOverlay:  "--ro-overlay",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	SyncFd:  "--sync-fd",
 | 
					 | 
				
			||||||
	Seccomp: "--seccomp",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	File:       "--file",
 | 
					 | 
				
			||||||
	BindData:   "--bind-data",
 | 
					 | 
				
			||||||
	ROBindData: "--ro-bind-data",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PermConfig[T FSBuilder] struct {
 | 
					 | 
				
			||||||
	// set permissions of next argument
 | 
					 | 
				
			||||||
	// (--perms OCTAL)
 | 
					 | 
				
			||||||
	Mode *os.FileMode `json:"mode,omitempty"`
 | 
					 | 
				
			||||||
	// path to get the new permission
 | 
					 | 
				
			||||||
	// (--bind-data, --file, etc.)
 | 
					 | 
				
			||||||
	Inner T `json:"path"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Len() int {
 | 
					 | 
				
			||||||
	if p.Mode != nil {
 | 
					 | 
				
			||||||
		return p.Inner.Len() + 2
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return p.Inner.Len()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *PermConfig[T]) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if p.Mode != nil {
 | 
					 | 
				
			||||||
		*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	p.Inner.Append(args)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type TmpfsConfig struct {
 | 
					 | 
				
			||||||
	// set size of tmpfs
 | 
					 | 
				
			||||||
	// (--size BYTES)
 | 
					 | 
				
			||||||
	Size int `json:"size,omitempty"`
 | 
					 | 
				
			||||||
	// mount point of new tmpfs
 | 
					 | 
				
			||||||
	// (--tmpfs DEST)
 | 
					 | 
				
			||||||
	Dir string `json:"dir"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Path() string { return t.Dir }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Len() int {
 | 
					 | 
				
			||||||
	if t.Size > 0 {
 | 
					 | 
				
			||||||
		return 4
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return 2
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (t *TmpfsConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if t.Size > 0 {
 | 
					 | 
				
			||||||
		*args = append(*args, Size.String(), strconv.Itoa(t.Size))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*args = append(*args, Tmpfs.String(), t.Dir)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type OverlayConfig struct {
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		read files from SRC in the following overlay
 | 
					 | 
				
			||||||
		(--overlay-src SRC)
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Src []string `json:"src,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		mount overlayfs on DEST, with RWSRC as the host path for writes and
 | 
					 | 
				
			||||||
		WORKDIR an empty directory on the same filesystem as RWSRC
 | 
					 | 
				
			||||||
		(--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
 | 
					 | 
				
			||||||
		(--tmp-overlay DEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if either strings are empty, mount overlayfs read-only on DEST
 | 
					 | 
				
			||||||
		(--ro-overlay DEST)
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Persist *[2]string `json:"persist,omitempty"`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		--overlay RWSRC WORKDIR DEST
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		--tmp-overlay DEST
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		--ro-overlay DEST
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	Dest string `json:"dest"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Path() string { return o.Dest }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Len() int {
 | 
					 | 
				
			||||||
	// (--tmp-overlay DEST) or (--ro-overlay DEST)
 | 
					 | 
				
			||||||
	p := 2
 | 
					 | 
				
			||||||
	// (--overlay RWSRC WORKDIR DEST)
 | 
					 | 
				
			||||||
	if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
 | 
					 | 
				
			||||||
		p = 4
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return p + len(o.Src)*2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (o *OverlayConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	// --overlay-src SRC
 | 
					 | 
				
			||||||
	for _, src := range o.Src {
 | 
					 | 
				
			||||||
		*args = append(*args, OverlaySrc.String(), src)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if o.Persist != nil {
 | 
					 | 
				
			||||||
		if o.Persist[0] != "" && o.Persist[1] != "" {
 | 
					 | 
				
			||||||
			// --overlay RWSRC WORKDIR
 | 
					 | 
				
			||||||
			*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// --ro-overlay
 | 
					 | 
				
			||||||
			*args = append(*args, ROOverlay.String())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// --tmp-overlay
 | 
					 | 
				
			||||||
		*args = append(*args, TmpOverlay.String())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// DEST
 | 
					 | 
				
			||||||
	*args = append(*args, o.Dest)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SymlinkConfig [2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Path() string          { return s[1] }
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Len() int              { return 3 }
 | 
					 | 
				
			||||||
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type ChmodConfig map[string]os.FileMode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c ChmodConfig) Len() int { return len(c) }
 | 
					 | 
				
			||||||
func (c ChmodConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for path, mode := range c {
 | 
					 | 
				
			||||||
		*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	DataWrite = iota
 | 
					 | 
				
			||||||
	DataBind
 | 
					 | 
				
			||||||
	DataROBind
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type DataConfig struct {
 | 
					 | 
				
			||||||
	Dest string `json:"dest"`
 | 
					 | 
				
			||||||
	Data []byte `json:"data,omitempty"`
 | 
					 | 
				
			||||||
	Type int    `json:"type"`
 | 
					 | 
				
			||||||
	proc.File
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (d *DataConfig) Path() string { return d.Dest }
 | 
					 | 
				
			||||||
func (d *DataConfig) Len() int {
 | 
					 | 
				
			||||||
	if d == nil || d.Data == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 3
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
 | 
					 | 
				
			||||||
	if d.File != nil {
 | 
					 | 
				
			||||||
		panic("file initialised twice")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	d.File = proc.NewWriterTo(d)
 | 
					 | 
				
			||||||
	return d.File.Init(fd, v)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
 | 
					 | 
				
			||||||
	n, err := w.Write(d.Data)
 | 
					 | 
				
			||||||
	return int64(n), err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (d *DataConfig) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if d == nil || d.Data == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	var a PositionalArg
 | 
					 | 
				
			||||||
	switch d.Type {
 | 
					 | 
				
			||||||
	case DataWrite:
 | 
					 | 
				
			||||||
		a = File
 | 
					 | 
				
			||||||
	case DataBind:
 | 
					 | 
				
			||||||
		a = BindData
 | 
					 | 
				
			||||||
	case DataROBind:
 | 
					 | 
				
			||||||
		a = ROBindData
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		panic(fmt.Sprintf("invalid type %d", a))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,249 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static boolean args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type BoolArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b BoolArg) Unwrap() []string {
 | 
					 | 
				
			||||||
	return boolArgs[b]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	UnshareAll BoolArg = iota
 | 
					 | 
				
			||||||
	UnshareUser
 | 
					 | 
				
			||||||
	UnshareIPC
 | 
					 | 
				
			||||||
	UnsharePID
 | 
					 | 
				
			||||||
	UnshareNet
 | 
					 | 
				
			||||||
	UnshareUTS
 | 
					 | 
				
			||||||
	UnshareCGroup
 | 
					 | 
				
			||||||
	ShareNet
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	UserNS
 | 
					 | 
				
			||||||
	Clearenv
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	NewSession
 | 
					 | 
				
			||||||
	DieWithParent
 | 
					 | 
				
			||||||
	AsInit
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var boolArgs = [...][]string{
 | 
					 | 
				
			||||||
	UnshareAll:    {"--unshare-all", "--unshare-user"},
 | 
					 | 
				
			||||||
	UnshareUser:   {"--unshare-user"},
 | 
					 | 
				
			||||||
	UnshareIPC:    {"--unshare-ipc"},
 | 
					 | 
				
			||||||
	UnsharePID:    {"--unshare-pid"},
 | 
					 | 
				
			||||||
	UnshareNet:    {"--unshare-net"},
 | 
					 | 
				
			||||||
	UnshareUTS:    {"--unshare-uts"},
 | 
					 | 
				
			||||||
	UnshareCGroup: {"--unshare-cgroup"},
 | 
					 | 
				
			||||||
	ShareNet:      {"--share-net"},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	UserNS:   {"--disable-userns", "--assert-userns-disabled"},
 | 
					 | 
				
			||||||
	Clearenv: {"--clearenv"},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	NewSession:    {"--new-session"},
 | 
					 | 
				
			||||||
	DieWithParent: {"--die-with-parent"},
 | 
					 | 
				
			||||||
	AsInit:        {"--as-pid-1"},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) boolArgs() Builder {
 | 
					 | 
				
			||||||
	b := boolArg{
 | 
					 | 
				
			||||||
		UserNS:   !c.UserNS,
 | 
					 | 
				
			||||||
		Clearenv: c.Clearenv,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		NewSession:    c.NewSession,
 | 
					 | 
				
			||||||
		DieWithParent: c.DieWithParent,
 | 
					 | 
				
			||||||
		AsInit:        c.AsInit,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if c.Unshare == nil {
 | 
					 | 
				
			||||||
		b[UnshareAll] = true
 | 
					 | 
				
			||||||
		b[ShareNet] = c.Net
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		b[UnshareUser] = c.Unshare.User
 | 
					 | 
				
			||||||
		b[UnshareIPC] = c.Unshare.IPC
 | 
					 | 
				
			||||||
		b[UnsharePID] = c.Unshare.PID
 | 
					 | 
				
			||||||
		b[UnshareNet] = c.Unshare.Net
 | 
					 | 
				
			||||||
		b[UnshareUTS] = c.Unshare.UTS
 | 
					 | 
				
			||||||
		b[UnshareCGroup] = c.Unshare.CGroup
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &b
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type boolArg [len(boolArgs)]bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *boolArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for i, v := range b {
 | 
					 | 
				
			||||||
		if v {
 | 
					 | 
				
			||||||
			l += len(boolArgs[i])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (b *boolArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, v := range b {
 | 
					 | 
				
			||||||
		if v {
 | 
					 | 
				
			||||||
			*args = append(*args, BoolArg(i).Unwrap()...)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static integer args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type IntArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (i IntArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return intArgs[i]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	UID IntArg = iota
 | 
					 | 
				
			||||||
	GID
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var intArgs = [...]string{
 | 
					 | 
				
			||||||
	UID: "--uid",
 | 
					 | 
				
			||||||
	GID: "--gid",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) intArgs() Builder {
 | 
					 | 
				
			||||||
	return &intArg{
 | 
					 | 
				
			||||||
		UID: c.UID,
 | 
					 | 
				
			||||||
		GID: c.GID,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type intArg [len(intArgs)]*int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *intArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, v := range n {
 | 
					 | 
				
			||||||
		if v != nil {
 | 
					 | 
				
			||||||
			l += 2
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *intArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, v := range n {
 | 
					 | 
				
			||||||
		if v != nil {
 | 
					 | 
				
			||||||
			*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static string args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type StringArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s StringArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return stringArgs[s]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	Hostname StringArg = iota
 | 
					 | 
				
			||||||
	Chdir
 | 
					 | 
				
			||||||
	UnsetEnv
 | 
					 | 
				
			||||||
	LockFile
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var stringArgs = [...]string{
 | 
					 | 
				
			||||||
	Hostname: "--hostname",
 | 
					 | 
				
			||||||
	Chdir:    "--chdir",
 | 
					 | 
				
			||||||
	UnsetEnv: "--unsetenv",
 | 
					 | 
				
			||||||
	LockFile: "--lock-file",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) stringArgs() Builder {
 | 
					 | 
				
			||||||
	n := stringArg{
 | 
					 | 
				
			||||||
		UnsetEnv: c.UnsetEnv,
 | 
					 | 
				
			||||||
		LockFile: c.LockFile,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if c.Hostname != "" {
 | 
					 | 
				
			||||||
		n[Hostname] = []string{c.Hostname}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if c.Chdir != "" {
 | 
					 | 
				
			||||||
		n[Chdir] = []string{c.Chdir}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type stringArg [len(stringArgs)][]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *stringArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, arg := range s {
 | 
					 | 
				
			||||||
		l += len(arg) * 2
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *stringArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, arg := range s {
 | 
					 | 
				
			||||||
		for _, v := range arg {
 | 
					 | 
				
			||||||
			*args = append(*args, StringArg(i).Unwrap(), v)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	static pair args
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type PairArg int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p PairArg) Unwrap() string {
 | 
					 | 
				
			||||||
	return pairArgs[p]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	SetEnv PairArg = iota
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var pairArgs = [...]string{
 | 
					 | 
				
			||||||
	SetEnv: "--setenv",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Config) pairArgs() Builder {
 | 
					 | 
				
			||||||
	var n pairArg
 | 
					 | 
				
			||||||
	n[SetEnv] = make([][2]string, len(c.SetEnv))
 | 
					 | 
				
			||||||
	keys := make([]string, 0, len(c.SetEnv))
 | 
					 | 
				
			||||||
	for k := range c.SetEnv {
 | 
					 | 
				
			||||||
		keys = append(keys, k)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	slices.Sort(keys)
 | 
					 | 
				
			||||||
	for i, k := range keys {
 | 
					 | 
				
			||||||
		n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type pairArg [len(pairArgs)][][2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairArg) Len() (l int) {
 | 
					 | 
				
			||||||
	for _, v := range p {
 | 
					 | 
				
			||||||
		l += len(v) * 3
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairArg) Append(args *[]string) {
 | 
					 | 
				
			||||||
	for i, arg := range p {
 | 
					 | 
				
			||||||
		for _, v := range arg {
 | 
					 | 
				
			||||||
			*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,52 +0,0 @@
 | 
				
			|||||||
package bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	gob.Register(new(pairF))
 | 
					 | 
				
			||||||
	gob.Register(new(stringF))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type pairF [3]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *pairF) Path() string          { return p[2] }
 | 
					 | 
				
			||||||
func (p *pairF) Len() int              { return len(p) }
 | 
					 | 
				
			||||||
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type stringF [2]string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s stringF) Path() string          { return s[1] }
 | 
					 | 
				
			||||||
func (s stringF) Len() int              { return len(s) /* compiler replaces this with 2 */ }
 | 
					 | 
				
			||||||
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type fileF struct {
 | 
					 | 
				
			||||||
	name string
 | 
					 | 
				
			||||||
	file *os.File
 | 
					 | 
				
			||||||
	proc.BaseFile
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) ErrCount() int                                  { return 0 }
 | 
					 | 
				
			||||||
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) Len() int {
 | 
					 | 
				
			||||||
	if f.file == nil {
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (f *fileF) Append(args *[]string) {
 | 
					 | 
				
			||||||
	if f.file == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,108 +0,0 @@
 | 
				
			|||||||
package helper_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestBwrap(t *testing.T) {
 | 
					 | 
				
			||||||
	sc := &bwrap.Config{
 | 
					 | 
				
			||||||
		Net:           true,
 | 
					 | 
				
			||||||
		Hostname:      "localhost",
 | 
					 | 
				
			||||||
		Chdir:         "/nonexistent",
 | 
					 | 
				
			||||||
		Clearenv:      true,
 | 
					 | 
				
			||||||
		NewSession:    true,
 | 
					 | 
				
			||||||
		DieWithParent: true,
 | 
					 | 
				
			||||||
		AsInit:        true,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("nonexistent bwrap name", func(t *testing.T) {
 | 
					 | 
				
			||||||
		bubblewrapName := helper.BubblewrapName
 | 
					 | 
				
			||||||
		helper.BubblewrapName = "/nonexistent"
 | 
					 | 
				
			||||||
		t.Cleanup(func() {
 | 
					 | 
				
			||||||
			helper.BubblewrapName = bubblewrapName
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "fortify", false,
 | 
					 | 
				
			||||||
			argsWt, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v, wantErr %v",
 | 
					 | 
				
			||||||
				err, os.ErrNotExist)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
					 | 
				
			||||||
		if got := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "fortify", false,
 | 
					 | 
				
			||||||
			argsWt, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		); got == nil {
 | 
					 | 
				
			||||||
			t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
 | 
					 | 
				
			||||||
				sc, argsWt, "fortify")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer func() {
 | 
					 | 
				
			||||||
			wantPanic := "argument contains null character"
 | 
					 | 
				
			||||||
			if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
				t.Errorf("MustNewBwrap: panic = %q, want %q",
 | 
					 | 
				
			||||||
					r, wantPanic)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			&bwrap.Config{Hostname: "\x00"}, "fortify", false,
 | 
					 | 
				
			||||||
			nil, argF,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("start without pipes", func(t *testing.T) {
 | 
					 | 
				
			||||||
		helper.InternalReplaceExecCommand(t)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		h := helper.MustNewBwrap(
 | 
					 | 
				
			||||||
			sc, "crash-test-dummy", false,
 | 
					 | 
				
			||||||
			nil, argFChecked,
 | 
					 | 
				
			||||||
			nil, nil,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
					 | 
				
			||||||
		defer cancel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(c, false); err != nil {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v",
 | 
					 | 
				
			||||||
				err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Wait(); err != nil {
 | 
					 | 
				
			||||||
			t.Errorf("Wait() err = %v stderr = %s",
 | 
					 | 
				
			||||||
				err, stderr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
					 | 
				
			||||||
		testHelper(t, func() helper.Helper {
 | 
					 | 
				
			||||||
			return helper.MustNewBwrap(
 | 
					 | 
				
			||||||
				sc, "crash-test-dummy", false,
 | 
					 | 
				
			||||||
				argsWt, argF, nil, nil,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										84
									
								
								helper/cmd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								helper/cmd.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
 | 
				
			||||||
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
 | 
					func NewDirect(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						cmdF func(cmd *exec.Cmd),
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) Helper {
 | 
				
			||||||
 | 
						d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						d.Args = append(d.Args, args...)
 | 
				
			||||||
 | 
						if cmdF != nil {
 | 
				
			||||||
 | 
							cmdF(d.Cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return d
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newHelperCmd(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) (cmd *helperCmd, args []string) {
 | 
				
			||||||
 | 
						cmd = new(helperCmd)
 | 
				
			||||||
 | 
						cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						cmd.Cmd = exec.CommandContext(ctx, name)
 | 
				
			||||||
 | 
						cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
 | 
				
			||||||
 | 
						cmd.WaitDelay = WaitDelay
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
 | 
				
			||||||
 | 
					type helperCmd struct {
 | 
				
			||||||
 | 
						mu sync.RWMutex
 | 
				
			||||||
 | 
						*helperFiles
 | 
				
			||||||
 | 
						*exec.Cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *helperCmd) Start() error {
 | 
				
			||||||
 | 
						h.mu.Lock()
 | 
				
			||||||
 | 
						defer h.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
				
			||||||
 | 
						// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
				
			||||||
 | 
						if h.Cmd != nil && h.Cmd.Process != nil {
 | 
				
			||||||
 | 
							return errors.New("helper: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h.Env = slices.Grow(h.Env, 2)
 | 
				
			||||||
 | 
						if h.useArgsFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=1")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if h.useStatFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stat is populated on fulfill
 | 
				
			||||||
 | 
							h.Cancel = func() error { return h.stat.Close() }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								helper/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								helper/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package helper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCmd(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("start non-existent helper path", func(t *testing.T) {
 | 
				
			||||||
 | 
							h := helper.NewDirect(context.Background(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
 | 
								t.Errorf("Start: error = %v, wantErr %v",
 | 
				
			||||||
 | 
									err, os.ErrNotExist)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
 | 
							if got := helper.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
 | 
				
			||||||
 | 
								t.Errorf("NewDirect(%q, %q) got nil",
 | 
				
			||||||
 | 
									argsWt, "fortify")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
 | 
							testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
 | 
				
			||||||
 | 
								return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
 | 
				
			||||||
 | 
									setOutput(&cmd.Stdout, &cmd.Stderr)
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										76
									
								
								helper/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								helper/container.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New initialises a Helper instance with wt as the null-terminated argument writer.
 | 
				
			||||||
 | 
					func New(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
 | 
						cmdF func(container *sandbox.Container),
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
					) Helper {
 | 
				
			||||||
 | 
						var args []string
 | 
				
			||||||
 | 
						h := new(helperContainer)
 | 
				
			||||||
 | 
						h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
 | 
				
			||||||
 | 
						h.Container = sandbox.New(ctx, name, args...)
 | 
				
			||||||
 | 
						h.WaitDelay = WaitDelay
 | 
				
			||||||
 | 
						if cmdF != nil {
 | 
				
			||||||
 | 
							cmdF(h.Container)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
 | 
				
			||||||
 | 
					type helperContainer struct {
 | 
				
			||||||
 | 
						started bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mu sync.Mutex
 | 
				
			||||||
 | 
						*helperFiles
 | 
				
			||||||
 | 
						*sandbox.Container
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *helperContainer) Start() error {
 | 
				
			||||||
 | 
						h.mu.Lock()
 | 
				
			||||||
 | 
						defer h.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if h.started {
 | 
				
			||||||
 | 
							return errors.New("helper: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.started = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h.Env = slices.Grow(h.Env, 2)
 | 
				
			||||||
 | 
						if h.useArgsFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=1")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyHelper+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if h.useStatFd {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stat is populated on fulfill
 | 
				
			||||||
 | 
							h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							h.Env = append(h.Env, FortifyStatus+"=0")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
 | 
				
			||||||
 | 
							if err := h.Container.Start(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return h.Container.Serve()
 | 
				
			||||||
 | 
						}, h.files, h.extraFiles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								helper/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								helper/container_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					package helper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainer(t *testing.T) {
 | 
				
			||||||
 | 
						t.Run("start empty container", func(t *testing.T) {
 | 
				
			||||||
 | 
							h := helper.New(context.Background(), "/nonexistent", argsWt, false, argF, nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							wantErr := "sandbox: starting an empty container"
 | 
				
			||||||
 | 
							if err := h.Start(); err == nil || err.Error() != wantErr {
 | 
				
			||||||
 | 
								t.Errorf("Start: error = %v, wantErr %q",
 | 
				
			||||||
 | 
									err, wantErr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
 | 
							if got := helper.New(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
 | 
				
			||||||
 | 
								t.Errorf("New(%q, %q) got nil",
 | 
				
			||||||
 | 
									argsWt, "fortify")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
 | 
							testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
 | 
				
			||||||
 | 
								return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *sandbox.Container) {
 | 
				
			||||||
 | 
									setOutput(&container.Stdout, &container.Stderr)
 | 
				
			||||||
 | 
									container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
 | 
				
			||||||
 | 
										return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
											"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									container.Bind("/", "/", 0)
 | 
				
			||||||
 | 
									container.Proc("/proc")
 | 
				
			||||||
 | 
									container.Dev("/dev")
 | 
				
			||||||
 | 
								}, nil)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						sandbox.Init(fmsg.Prepare, func(bool) { internal.InstallFmsg(false) })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,40 +0,0 @@
 | 
				
			|||||||
package helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// direct wraps *exec.Cmd and manages status and args fd.
 | 
					 | 
				
			||||||
// Args is always 3 and status if set is always 4.
 | 
					 | 
				
			||||||
type direct struct {
 | 
					 | 
				
			||||||
	lock sync.RWMutex
 | 
					 | 
				
			||||||
	*helperCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (h *direct) Start(ctx context.Context, stat bool) error {
 | 
					 | 
				
			||||||
	h.lock.Lock()
 | 
					 | 
				
			||||||
	defer h.lock.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check for doubled Start calls before we defer failure cleanup. If the prior
 | 
					 | 
				
			||||||
	// call to Start succeeded, we don't want to spuriously close its pipes.
 | 
					 | 
				
			||||||
	if h.Cmd != nil && h.Cmd.Process != nil {
 | 
					 | 
				
			||||||
		return errors.New("exec: already started")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	args := h.finalise(ctx, stat)
 | 
					 | 
				
			||||||
	h.Cmd.Args = append(h.Cmd.Args, args...)
 | 
					 | 
				
			||||||
	return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New initialises a new direct Helper instance with wt as the null-terminated argument writer.
 | 
					 | 
				
			||||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
					 | 
				
			||||||
func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
 | 
					 | 
				
			||||||
	d := new(direct)
 | 
					 | 
				
			||||||
	d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
 | 
					 | 
				
			||||||
	return d
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,33 +0,0 @@
 | 
				
			|||||||
package helper_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestDirect(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Run("start non-existent helper path", func(t *testing.T) {
 | 
					 | 
				
			||||||
		h := helper.New(argsWt, "/nonexistent", argF)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
 | 
					 | 
				
			||||||
			t.Errorf("Start: error = %v, wantErr %v",
 | 
					 | 
				
			||||||
				err, os.ErrNotExist)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
					 | 
				
			||||||
		if got := helper.New(argsWt, "fortify", argF); got == nil {
 | 
					 | 
				
			||||||
			t.Errorf("New(%q, %q) got nil",
 | 
					 | 
				
			||||||
				argsWt, "fortify")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
					 | 
				
			||||||
		testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										117
									
								
								helper/helper.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								helper/helper.go
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
				
			|||||||
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
 | 
					// Package helper runs external helpers with optional sandboxing.
 | 
				
			||||||
package helper
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
@ -6,17 +6,12 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"slices"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
						"git.gensokyo.uk/security/fortify/helper/proc"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var WaitDelay = 2 * time.Second
 | 
				
			||||||
	WaitDelay = 2 * time.Second
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
 | 
						// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
 | 
				
			||||||
@ -26,62 +21,56 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Helper interface {
 | 
					type Helper interface {
 | 
				
			||||||
	// Stdin sets the standard input of Helper.
 | 
					 | 
				
			||||||
	Stdin(r io.Reader) Helper
 | 
					 | 
				
			||||||
	// Stdout sets the standard output of Helper.
 | 
					 | 
				
			||||||
	Stdout(w io.Writer) Helper
 | 
					 | 
				
			||||||
	// Stderr sets the standard error of Helper.
 | 
					 | 
				
			||||||
	Stderr(w io.Writer) Helper
 | 
					 | 
				
			||||||
	// SetEnv sets the environment of Helper.
 | 
					 | 
				
			||||||
	SetEnv(env []string) Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Start starts the helper process.
 | 
						// Start starts the helper process.
 | 
				
			||||||
	// A status pipe is passed to the helper if stat is true.
 | 
						Start() error
 | 
				
			||||||
	Start(ctx context.Context, stat bool) error
 | 
						// Wait blocks until Helper exits.
 | 
				
			||||||
	// Wait blocks until Helper exits and releases all its resources.
 | 
					 | 
				
			||||||
	Wait() error
 | 
						Wait() error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Stringer
 | 
						fmt.Stringer
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newHelperCmd(
 | 
					func newHelperFiles(
 | 
				
			||||||
	h Helper, name string,
 | 
						ctx context.Context,
 | 
				
			||||||
	wt io.WriterTo, argF func(argsFd, statFd int) []string,
 | 
						wt io.WriterTo,
 | 
				
			||||||
 | 
						stat bool,
 | 
				
			||||||
 | 
						argF func(argsFd, statFd int) []string,
 | 
				
			||||||
	extraFiles []*os.File,
 | 
						extraFiles []*os.File,
 | 
				
			||||||
) (cmd *helperCmd) {
 | 
					) (hl *helperFiles, args []string) {
 | 
				
			||||||
	cmd = new(helperCmd)
 | 
						hl = new(helperFiles)
 | 
				
			||||||
 | 
						hl.ctx = ctx
 | 
				
			||||||
 | 
						hl.useArgsFd = wt != nil
 | 
				
			||||||
 | 
						hl.useStatFd = stat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cmd.r = h
 | 
						hl.extraFiles = new(proc.ExtraFilesPre)
 | 
				
			||||||
	cmd.name = name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cmd.extraFiles = new(proc.ExtraFilesPre)
 | 
					 | 
				
			||||||
	for _, f := range extraFiles {
 | 
						for _, f := range extraFiles {
 | 
				
			||||||
		_, v := cmd.extraFiles.Append()
 | 
							_, v := hl.extraFiles.Append()
 | 
				
			||||||
		*v = f
 | 
							*v = f
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	argsFd := -1
 | 
						argsFd := -1
 | 
				
			||||||
	if wt != nil {
 | 
						if hl.useArgsFd {
 | 
				
			||||||
		f := proc.NewWriterTo(wt)
 | 
							f := proc.NewWriterTo(wt)
 | 
				
			||||||
		argsFd = int(proc.InitFile(f, cmd.extraFiles))
 | 
							argsFd = int(proc.InitFile(f, hl.extraFiles))
 | 
				
			||||||
		cmd.files = append(cmd.files, f)
 | 
							hl.files = append(hl.files, f)
 | 
				
			||||||
		cmd.hasArgsFd = true
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						statFd := -1
 | 
				
			||||||
 | 
						if hl.useStatFd {
 | 
				
			||||||
 | 
							f := proc.NewStat(&hl.stat)
 | 
				
			||||||
 | 
							statFd = int(proc.InitFile(f, hl.extraFiles))
 | 
				
			||||||
 | 
							hl.files = append(hl.files, f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args = argF(argsFd, statFd)
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
 | 
					// helperFiles provides a generic wrapper around helper ipc.
 | 
				
			||||||
type helperCmd struct {
 | 
					type helperFiles struct {
 | 
				
			||||||
	// ref to parent
 | 
					 | 
				
			||||||
	r Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// returns an array of arguments passed directly
 | 
					 | 
				
			||||||
	// to the helper process
 | 
					 | 
				
			||||||
	argF func(statFd int) []string
 | 
					 | 
				
			||||||
	// whether argsFd is present
 | 
						// whether argsFd is present
 | 
				
			||||||
	hasArgsFd bool
 | 
						useArgsFd bool
 | 
				
			||||||
 | 
						// whether statFd is present
 | 
				
			||||||
 | 
						useStatFd bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// closes statFd
 | 
						// closes statFd
 | 
				
			||||||
	stat io.Closer
 | 
						stat io.Closer
 | 
				
			||||||
@ -90,45 +79,5 @@ type helperCmd struct {
 | 
				
			|||||||
	// passed through to [proc.Fulfill] and [proc.InitFile]
 | 
						// passed through to [proc.Fulfill] and [proc.InitFile]
 | 
				
			||||||
	extraFiles *proc.ExtraFilesPre
 | 
						extraFiles *proc.ExtraFilesPre
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	name           string
 | 
						ctx context.Context
 | 
				
			||||||
	stdin          io.Reader
 | 
					 | 
				
			||||||
	stdout, stderr io.Writer
 | 
					 | 
				
			||||||
	env            []string
 | 
					 | 
				
			||||||
	*exec.Cmd
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func (h *helperCmd) Stdin(r io.Reader) Helper   { h.stdin = r; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) Stdout(w io.Writer) Helper  { h.stdout = w; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) Stderr(w io.Writer) Helper  { h.stderr = w; return h.r }
 | 
					 | 
				
			||||||
func (h *helperCmd) SetEnv(env []string) Helper { h.env = env; return h.r }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// finalise initialises the underlying [exec.Cmd] object.
 | 
					 | 
				
			||||||
func (h *helperCmd) finalise(ctx context.Context, stat bool) (args []string) {
 | 
					 | 
				
			||||||
	h.Cmd = commandContext(ctx, h.name)
 | 
					 | 
				
			||||||
	h.Cmd.Stdin, h.Cmd.Stdout, h.Cmd.Stderr = h.stdin, h.stdout, h.stderr
 | 
					 | 
				
			||||||
	h.Cmd.Env = slices.Grow(h.env, 2)
 | 
					 | 
				
			||||||
	if h.hasArgsFd {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=0")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	h.Cmd.Cancel = func() error { return h.Cmd.Process.Signal(syscall.SIGTERM) }
 | 
					 | 
				
			||||||
	h.Cmd.WaitDelay = WaitDelay
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	statFd := -1
 | 
					 | 
				
			||||||
	if stat {
 | 
					 | 
				
			||||||
		f := proc.NewStat(&h.stat)
 | 
					 | 
				
			||||||
		statFd = int(proc.InitFile(f, h.extraFiles))
 | 
					 | 
				
			||||||
		h.files = append(h.files, f)
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=1")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// stat is populated on fulfill
 | 
					 | 
				
			||||||
		h.Cmd.Cancel = func() error { return h.stat.Close() }
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=0")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return h.argF(statFd)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var commandContext = exec.CommandContext
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
@ -35,7 +36,8 @@ func argF(argsFd, statFd int) []string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func argFChecked(argsFd, statFd int) (args []string) {
 | 
					func argFChecked(argsFd, statFd int) (args []string) {
 | 
				
			||||||
	args = make([]string, 0, 4)
 | 
						args = make([]string, 0, 6)
 | 
				
			||||||
 | 
						args = append(args, "-test.run=TestHelperStub", "--")
 | 
				
			||||||
	if argsFd > -1 {
 | 
						if argsFd > -1 {
 | 
				
			||||||
		args = append(args, "--args", strconv.Itoa(argsFd))
 | 
							args = append(args, "--args", strconv.Itoa(argsFd))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -46,14 +48,15 @@ func argFChecked(argsFd, statFd int) (args []string) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this function tests an implementation of the helper.Helper interface
 | 
					// this function tests an implementation of the helper.Helper interface
 | 
				
			||||||
func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
					func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
 | 
				
			||||||
	helper.InternalReplaceExecCommand(t)
 | 
						oldWaitDelay := helper.WaitDelay
 | 
				
			||||||
 | 
						helper.WaitDelay = 16 * time.Second
 | 
				
			||||||
 | 
						t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start helper with status channel and wait", func(t *testing.T) {
 | 
						t.Run("start helper with status channel and wait", func(t *testing.T) {
 | 
				
			||||||
		h := createHelper()
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
							stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
							h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("wait not yet started helper", func(t *testing.T) {
 | 
							t.Run("wait not yet started helper", func(t *testing.T) {
 | 
				
			||||||
			defer func() {
 | 
								defer func() {
 | 
				
			||||||
@ -65,10 +68,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
			panic(fmt.Sprintf("unreachable: %v", h.Wait()))
 | 
								panic(fmt.Sprintf("unreachable: %v", h.Wait()))
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		t.Log("starting helper stub")
 | 
							t.Log("starting helper stub")
 | 
				
			||||||
		if err := h.Start(ctx, true); err != nil {
 | 
							if err := h.Start(); err != nil {
 | 
				
			||||||
			t.Errorf("Start: error = %v", err)
 | 
								t.Errorf("Start: error = %v", err)
 | 
				
			||||||
			cancel()
 | 
								cancel()
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@ -77,8 +78,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
		cancel()
 | 
							cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		t.Run("start already started helper", func(t *testing.T) {
 | 
							t.Run("start already started helper", func(t *testing.T) {
 | 
				
			||||||
			wantErr := "exec: already started"
 | 
								wantErr := "helper: already started"
 | 
				
			||||||
			if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
 | 
								if err := h.Start(); err != nil && err.Error() != wantErr {
 | 
				
			||||||
				t.Errorf("Start: error = %v, wantErr %v",
 | 
									t.Errorf("Start: error = %v, wantErr %v",
 | 
				
			||||||
					err, wantErr)
 | 
										err, wantErr)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -100,21 +101,19 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
 | 
							if got := stderr.String(); got != wantPayload {
 | 
				
			||||||
			t.Errorf("Start: stdout = %v, want %v",
 | 
								t.Errorf("Start: stderr = %v, want %v",
 | 
				
			||||||
				got, wantPayload)
 | 
									got, wantPayload)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start helper and wait", func(t *testing.T) {
 | 
						t.Run("start helper and wait", func(t *testing.T) {
 | 
				
			||||||
		h := createHelper()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
		defer cancel()
 | 
							defer cancel()
 | 
				
			||||||
 | 
							stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
				
			||||||
 | 
							h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := h.Start(ctx, false); err != nil {
 | 
							if err := h.Start(); err != nil {
 | 
				
			||||||
			t.Errorf("Start() error = %v",
 | 
								t.Errorf("Start() error = %v",
 | 
				
			||||||
				err)
 | 
									err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@ -125,8 +124,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
 | 
				
			|||||||
				err, stdout, stderr)
 | 
									err, stdout, stderr)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
 | 
							if got := stderr.String(); got != wantPayload {
 | 
				
			||||||
			t.Errorf("Start() stdout = %v, want %v",
 | 
								t.Errorf("Start() stderr = %v, want %v",
 | 
				
			||||||
				got, wantPayload)
 | 
									got, wantPayload)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
				
			|||||||
@ -60,7 +60,10 @@ func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
 | 
					// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
 | 
				
			||||||
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
 | 
					func Fulfill(ctx context.Context,
 | 
				
			||||||
 | 
						v *[]*os.File, start func() error,
 | 
				
			||||||
 | 
						files []File, extraFiles *ExtraFilesPre,
 | 
				
			||||||
 | 
					) (err error) {
 | 
				
			||||||
	var ecs int
 | 
						var ecs int
 | 
				
			||||||
	for _, o := range files {
 | 
						for _, o := range files {
 | 
				
			||||||
		ecs += o.ErrCount()
 | 
							ecs += o.ErrCount()
 | 
				
			||||||
@ -77,8 +80,8 @@ func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *Extra
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cmd.ExtraFiles = extraFiles.Files()
 | 
						*v = extraFiles.Files()
 | 
				
			||||||
	if err = cmd.Start(); err != nil {
 | 
						if err = start(); err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,17 @@
 | 
				
			|||||||
package helper
 | 
					package helper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"flag"
 | 
						"flag"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// InternalChildStub is an internal function but exported because it is cross-package;
 | 
					// InternalHelperStub is an internal function but exported because it is cross-package;
 | 
				
			||||||
// it is part of the implementation of the helper stub.
 | 
					// it is part of the implementation of the helper stub.
 | 
				
			||||||
func InternalChildStub() {
 | 
					func InternalHelperStub() {
 | 
				
			||||||
	// this test mocks the helper process
 | 
						// this test mocks the helper process
 | 
				
			||||||
	var ap, sp string
 | 
						var ap, sp string
 | 
				
			||||||
	if v, ok := os.LookupEnv(FortifyHelper); !ok {
 | 
						if v, ok := os.LookupEnv(FortifyHelper); !ok {
 | 
				
			||||||
@ -33,30 +25,9 @@ func InternalChildStub() {
 | 
				
			|||||||
		sp = v
 | 
							sp = v
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch os.Args[3] {
 | 
						genericStub(flagRestoreFiles(3, ap, sp))
 | 
				
			||||||
	case "bwrap":
 | 
					 | 
				
			||||||
		bwrapStub()
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		genericStub(flagRestoreFiles(4, ap, sp))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal.Exit(0)
 | 
						os.Exit(0)
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
 | 
					 | 
				
			||||||
// it is part of the implementation of the helper stub.
 | 
					 | 
				
			||||||
func InternalReplaceExecCommand(t *testing.T) {
 | 
					 | 
				
			||||||
	t.Cleanup(func() { commandContext = exec.CommandContext })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
 | 
					 | 
				
			||||||
	commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
 | 
					 | 
				
			||||||
		// pass through nonexistent path
 | 
					 | 
				
			||||||
		if name == "/nonexistent" && len(arg) == 0 {
 | 
					 | 
				
			||||||
			return exec.CommandContext(ctx, name)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newFile(fd int, name, p string) *os.File {
 | 
					func newFile(fd int, name, p string) *os.File {
 | 
				
			||||||
@ -92,7 +63,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
 | 
				
			|||||||
func genericStub(argsFile, statFile *os.File) {
 | 
					func genericStub(argsFile, statFile *os.File) {
 | 
				
			||||||
	if argsFile != nil {
 | 
						if argsFile != nil {
 | 
				
			||||||
		// this output is checked by parent
 | 
							// this output is checked by parent
 | 
				
			||||||
		if _, err := io.Copy(os.Stdout, argsFile); err != nil {
 | 
							if _, err := io.Copy(os.Stderr, argsFile); err != nil {
 | 
				
			||||||
			panic("cannot read args: " + err.Error())
 | 
								panic("cannot read args: " + err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -133,42 +104,3 @@ func genericStub(argsFile, statFile *os.File) {
 | 
				
			|||||||
		<-done
 | 
							<-done
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func bwrapStub() {
 | 
					 | 
				
			||||||
	// the bwrap launcher does not launch with a typical sync fd
 | 
					 | 
				
			||||||
	argsFile, _ := flagRestoreFiles(4, "1", "0")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// test args pipe behaviour
 | 
					 | 
				
			||||||
	func() {
 | 
					 | 
				
			||||||
		got, want := new(strings.Builder), new(strings.Builder)
 | 
					 | 
				
			||||||
		if _, err := io.Copy(got, argsFile); err != nil {
 | 
					 | 
				
			||||||
			panic("cannot read bwrap args: " + err.Error())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// hardcoded bwrap configuration used by test
 | 
					 | 
				
			||||||
		sc := &bwrap.Config{
 | 
					 | 
				
			||||||
			Net:           true,
 | 
					 | 
				
			||||||
			Hostname:      "localhost",
 | 
					 | 
				
			||||||
			Chdir:         "/nonexistent",
 | 
					 | 
				
			||||||
			Clearenv:      true,
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
			AsInit:        true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
 | 
					 | 
				
			||||||
			WriteTo(want); err != nil {
 | 
					 | 
				
			||||||
			panic("cannot read want: " + err.Error())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
 | 
					 | 
				
			||||||
			panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := syscall.Exec(
 | 
					 | 
				
			||||||
		os.Args[0],
 | 
					 | 
				
			||||||
		append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
 | 
					 | 
				
			||||||
		os.Environ()); err != nil {
 | 
					 | 
				
			||||||
		panic("cannot start general stub: " + err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,4 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/helper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestHelperChildStub(t *testing.T) {
 | 
					func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
 | 
				
			||||||
	helper.InternalChildStub()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
@ -10,9 +11,10 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func New(os sys.State) (fst.App, error) {
 | 
					func New(ctx context.Context, os sys.State) (fst.App, error) {
 | 
				
			||||||
	a := new(app)
 | 
						a := new(app)
 | 
				
			||||||
	a.sys = os
 | 
						a.sys = os
 | 
				
			||||||
 | 
						a.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	id := new(fst.ID)
 | 
						id := new(fst.ID)
 | 
				
			||||||
	err := fst.NewAppID(id)
 | 
						err := fst.NewAppID(id)
 | 
				
			||||||
@ -21,8 +23,8 @@ func New(os sys.State) (fst.App, error) {
 | 
				
			|||||||
	return a, err
 | 
						return a, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func MustNew(os sys.State) fst.App {
 | 
					func MustNew(ctx context.Context, os sys.State) fst.App {
 | 
				
			||||||
	a, err := New(os)
 | 
						a, err := New(ctx, os)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatalf("cannot create app: %v", err)
 | 
							log.Fatalf("cannot create app: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -32,6 +34,7 @@ func MustNew(os sys.State) fst.App {
 | 
				
			|||||||
type app struct {
 | 
					type app struct {
 | 
				
			||||||
	id  *stringPair[fst.ID]
 | 
						id  *stringPair[fst.ID]
 | 
				
			||||||
	sys sys.State
 | 
						sys sys.State
 | 
				
			||||||
 | 
						ctx context.Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	*outcome
 | 
						*outcome
 | 
				
			||||||
	mu sync.RWMutex
 | 
						mu sync.RWMutex
 | 
				
			||||||
@ -71,7 +74,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	seal := new(outcome)
 | 
						seal := new(outcome)
 | 
				
			||||||
	seal.id = a.id
 | 
						seal.id = a.id
 | 
				
			||||||
	err := seal.finalise(a.sys, config)
 | 
						err := seal.finalise(a.ctx, a.sys, config)
 | 
				
			||||||
	if err == nil {
 | 
						if err == nil {
 | 
				
			||||||
		a.outcome = seal
 | 
							a.outcome = seal
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,20 +12,20 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos chromium direct wayland", new(stubNixOS),
 | 
							"nixos chromium direct wayland", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
								Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
									AppID: 1, Groups: []string{}, Username: "u0_a1",
 | 
				
			||||||
				Outer: "/var/lib/persist/module/fortify/0/1",
 | 
									Outer: "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				Sandbox: &fst.SandboxConfig{
 | 
									Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
					UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
										Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
 | 
				
			||||||
					Filesystem: []*fst.FilesystemConfig{
 | 
										Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
						{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
											{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
 | 
				
			||||||
						{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
											{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
 | 
				
			||||||
						{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
											{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
 | 
				
			||||||
						{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
											{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					Override: []string{"/var/run/nscd"},
 | 
										Cover: []string{"/var/run/nscd"},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				SystemBus: &dbus.Config{
 | 
									SystemBus: &dbus.Config{
 | 
				
			||||||
					Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
										Talk:   []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
				
			||||||
@ -45,7 +45,7 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
					Call: map[string]string{}, Broadcast: map[string]string{},
 | 
										Call: map[string]string{}, Broadcast: map[string]string{},
 | 
				
			||||||
					Filter: true,
 | 
										Filter: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
									Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		fst.ID{
 | 
							fst.ID{
 | 
				
			||||||
@ -88,136 +88,132 @@ var testCasesNixos = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Uid:   1971,
 | 
				
			||||||
			UserNS:   true,
 | 
								Gid:   100,
 | 
				
			||||||
			Chdir:    "/var/lib/persist/module/fortify/0/1",
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
 | 
				
			||||||
			Clearenv: true,
 | 
								Dir:   "/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
								Path:  "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
 | 
								Args:  []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
								Env: []string{
 | 
				
			||||||
				"HOME":                     "/var/lib/persist/module/fortify/0/1",
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/1971/pulse/native",
 | 
									"HOME=/var/lib/persist/module/fortify/0/1",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"PULSE_SERVER=unix:/run/user/1971/pulse/native",
 | 
				
			||||||
				"USER":                     "u0_a1",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"USER=u0_a1",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/1971",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
									"XDG_RUNTIME_DIR=/run/user/1971",
 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			NewSession:    true,
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(1971).SetGID(1971).
 | 
									Bind("/bin", "/bin", 0).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/usr/bin", "/usr/bin", 0).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/nix/store", "/nix/store", 0).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/run/current-system", "/run/current-system", 0).
 | 
				
			||||||
			Bind("/bin", "/bin").
 | 
									Bind("/sys/block", "/sys/block", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr/bin", "/usr/bin").
 | 
									Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/nix/store", "/nix/store").
 | 
									Bind("/sys/class", "/sys/class", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/run/current-system", "/run/current-system").
 | 
									Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/block", "/sys/block", true).
 | 
									Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/bus", "/sys/bus", true).
 | 
									Bind("/run/opengl-driver", "/run/opengl-driver", 0).
 | 
				
			||||||
			Bind("/sys/class", "/sys/class", true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys/dev", "/sys/dev", true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/sys/devices", "/sys/devices", true).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Bind("/run/opengl-driver", "/run/opengl-driver").
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user/1971", 8388608, 0700).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8388608).
 | 
									Place("/etc/group", []byte("fortify:x:100:\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
 | 
									Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
 | 
				
			||||||
			Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
 | 
									Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
 | 
				
			||||||
			Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
 | 
									Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
							},
 | 
				
			||||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
 | 
					 | 
				
			||||||
			Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,6 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos permissive defaults no enablements", new(stubNixOS),
 | 
							"nixos permissive defaults no enablements", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			Command: make([]string, 0),
 | 
					 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    0,
 | 
									AppID:    0,
 | 
				
			||||||
				Username: "chronos",
 | 
									Username: "chronos",
 | 
				
			||||||
@ -35,136 +34,131 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
								Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
								Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
 | 
				
			||||||
			Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
								Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Clearenv: true,
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Args:  []string{"/run/current-system/sw/bin/zsh"},
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":              "/home/chronos",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":             "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":              "xterm-256color",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"USER":              "chronos",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":   "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS": "user",
 | 
								},
 | 
				
			||||||
				"XDG_SESSION_TYPE":  "tty"},
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
									Proc("/proc").
 | 
				
			||||||
			DieWithParent: true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
			AsInit:        true,
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user/65534", 8388608, 0700).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
							},
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
					 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		"nixos permissive defaults chromium", new(stubNixOS),
 | 
							"nixos permissive defaults chromium", new(stubNixOS),
 | 
				
			||||||
		&fst.Config{
 | 
							&fst.Config{
 | 
				
			||||||
			ID:      "org.chromium.Chromium",
 | 
								ID:   "org.chromium.Chromium",
 | 
				
			||||||
			Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
 | 
								Args: []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Confinement: fst.ConfinementConfig{
 | 
								Confinement: fst.ConfinementConfig{
 | 
				
			||||||
				AppID:    9,
 | 
									AppID:    9,
 | 
				
			||||||
				Groups:   []string{"video"},
 | 
									Groups:   []string{"video"},
 | 
				
			||||||
@ -201,7 +195,7 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
					},
 | 
										},
 | 
				
			||||||
					Filter: true,
 | 
										Filter: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
 | 
									Enablements: system.EWayland | system.EDBus | system.EPulse,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		fst.ID{
 | 
							fst.ID{
 | 
				
			||||||
@ -254,141 +248,135 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			}).
 | 
								}).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
 | 
				
			||||||
			UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
								UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
 | 
				
			||||||
		(&bwrap.Config{
 | 
							&sandbox.Params{
 | 
				
			||||||
			Net:      true,
 | 
								Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
 | 
				
			||||||
			UserNS:   true,
 | 
								Dir:   "/home/chronos",
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Path:  "/run/current-system/sw/bin/zsh",
 | 
				
			||||||
			Clearenv: true,
 | 
								Args:  []string{"zsh", "-c", "exec chromium "},
 | 
				
			||||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
								Env: []string{
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
									"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
									"HOME=/home/chronos",
 | 
				
			||||||
				"HOME":                     "/home/chronos",
 | 
									"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
 | 
				
			||||||
				"PULSE_COOKIE":             fst.Tmp + "/pulse-cookie",
 | 
									"PULSE_SERVER=unix:/run/user/65534/pulse/native",
 | 
				
			||||||
				"PULSE_SERVER":             "unix:/run/user/65534/pulse/native",
 | 
									"TERM=xterm-256color",
 | 
				
			||||||
				"SHELL":                    "/run/current-system/sw/bin/zsh",
 | 
									"USER=chronos",
 | 
				
			||||||
				"TERM":                     "xterm-256color",
 | 
									"WAYLAND_DISPLAY=wayland-0",
 | 
				
			||||||
				"USER":                     "chronos",
 | 
									"XDG_RUNTIME_DIR=/run/user/65534",
 | 
				
			||||||
				"WAYLAND_DISPLAY":          "wayland-0",
 | 
									"XDG_SESSION_CLASS=user",
 | 
				
			||||||
				"XDG_RUNTIME_DIR":          "/run/user/65534",
 | 
									"XDG_SESSION_TYPE=tty",
 | 
				
			||||||
				"XDG_SESSION_CLASS":        "user",
 | 
					 | 
				
			||||||
				"XDG_SESSION_TYPE":         "tty",
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Chmod:         make(bwrap.ChmodConfig),
 | 
								Ops: new(sandbox.Ops).
 | 
				
			||||||
			DieWithParent: true,
 | 
									Proc("/proc").
 | 
				
			||||||
			AsInit:        true,
 | 
									Tmpfs(fst.Tmp, 4096, 0755).
 | 
				
			||||||
		}).SetUID(65534).SetGID(65534).
 | 
									Dev("/dev").Mqueue("/dev/mqueue").
 | 
				
			||||||
			Procfs("/proc").
 | 
									Bind("/bin", "/bin", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs(fst.Tmp, 4096).
 | 
									Bind("/boot", "/boot", sandbox.BindWritable).
 | 
				
			||||||
			DevTmpfs("/dev").Mqueue("/dev/mqueue").
 | 
									Bind("/home", "/home", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/bin", "/bin", false, true).
 | 
									Bind("/lib", "/lib", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/boot", "/boot", false, true).
 | 
									Bind("/lib64", "/lib64", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/home", "/home", false, true).
 | 
									Bind("/nix", "/nix", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib", "/lib", false, true).
 | 
									Bind("/root", "/root", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/lib64", "/lib64", false, true).
 | 
									Bind("/run", "/run", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/nix", "/nix", false, true).
 | 
									Bind("/srv", "/srv", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/root", "/root", false, true).
 | 
									Bind("/sys", "/sys", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/run", "/run", false, true).
 | 
									Bind("/usr", "/usr", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/srv", "/srv", false, true).
 | 
									Bind("/var", "/var", sandbox.BindWritable).
 | 
				
			||||||
			Bind("/sys", "/sys", false, true).
 | 
									Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/usr", "/usr", false, true).
 | 
									Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
 | 
				
			||||||
			Bind("/var", "/var", false, true).
 | 
									Tmpfs("/run/user/1971", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/dri", "/dev/dri", true, true, true).
 | 
									Tmpfs("/run/dbus", 8192, 0755).
 | 
				
			||||||
			Bind("/dev/kvm", "/dev/kvm", true, true, true).
 | 
									Bind("/etc", fst.Tmp+"/etc", 0).
 | 
				
			||||||
			Tmpfs("/run/user/1971", 8192).
 | 
									Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
				
			||||||
			Tmpfs("/run/dbus", 8192).
 | 
									Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
				
			||||||
			Bind("/etc", fst.Tmp+"/etc").
 | 
									Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
 | 
									Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
 | 
									Link(fst.Tmp+"/etc/default", "/etc/default").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
 | 
									Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
 | 
									Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/default", "/etc/default").
 | 
									Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
 | 
									Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
 | 
									Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
 | 
									Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
 | 
									Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
 | 
									Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
 | 
									Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
 | 
									Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
 | 
									Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
 | 
									Link(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
 | 
									Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
 | 
									Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
 | 
									Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
 | 
									Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
 | 
									Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
 | 
									Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
 | 
									Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
 | 
									Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
 | 
									Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
 | 
									Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
 | 
									Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
 | 
									Link("/proc/mounts", "/etc/mtab").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
 | 
									Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
 | 
									Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
				
			||||||
			Symlink("/proc/mounts", "/etc/mtab").
 | 
									Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
 | 
									Link(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
 | 
									Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
 | 
									Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
 | 
									Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
 | 
									Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
 | 
									Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
 | 
									Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
 | 
									Link(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
 | 
									Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
 | 
									Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
 | 
									Link(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
 | 
									Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
 | 
									Link(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
 | 
									Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
 | 
									Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
 | 
									Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
 | 
									Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
 | 
									Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
 | 
									Link(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
 | 
									Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
 | 
									Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
 | 
									Link(fst.Tmp+"/etc/services", "/etc/services").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
 | 
									Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
 | 
									Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/services", "/etc/services").
 | 
									Link(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
 | 
									Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
 | 
									Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
 | 
									Link(fst.Tmp+"/etc/static", "/etc/static").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
 | 
									Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
 | 
									Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/static", "/etc/static").
 | 
									Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
 | 
									Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
 | 
									Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
 | 
									Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
 | 
									Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
 | 
									Link(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
 | 
									Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
 | 
									Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
 | 
									Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
 | 
									Link(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
 | 
									Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
 | 
									Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
 | 
									Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
 | 
									Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
 | 
									Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
 | 
									Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
 | 
									Tmpfs("/run/user", 4096, 0755).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
 | 
									Tmpfs("/run/user/65534", 8388608, 0700).
 | 
				
			||||||
			Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
 | 
									Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user", 1048576).
 | 
									Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
 | 
				
			||||||
			Tmpfs("/run/user/65534", 8388608).
 | 
									Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
 | 
									Place("/etc/group", []byte("fortify:x:65534:\n")).
 | 
				
			||||||
			Bind("/home/chronos", "/home/chronos", false, true).
 | 
									Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
 | 
				
			||||||
			CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
 | 
									Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
 | 
				
			||||||
			CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
 | 
									Place(fst.Tmp+"/pulse-cookie", nil).
 | 
				
			||||||
			Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
 | 
				
			||||||
			Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
 | 
									Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
 | 
				
			||||||
			CopyBind(fst.Tmp+"/pulse-cookie", nil).
 | 
									Tmpfs("/var/run/nscd", 8192, 0755),
 | 
				
			||||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
 | 
							},
 | 
				
			||||||
			Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
 | 
					 | 
				
			||||||
			Tmpfs("/var/run/nscd", 8192).
 | 
					 | 
				
			||||||
			Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
 | 
					 | 
				
			||||||
			Symlink("fortify", "/.fortify/sbin/init"),
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,8 @@ type stubNixOS struct {
 | 
				
			|||||||
	usernameErr map[string]error
 | 
						usernameErr map[string]error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *stubNixOS) Geteuid() int                             { return 1971 }
 | 
					func (s *stubNixOS) Getuid() int                              { return 1971 }
 | 
				
			||||||
 | 
					func (s *stubNixOS) Getgid() int                              { return 100 }
 | 
				
			||||||
func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
					func (s *stubNixOS) TempDir() string                          { return "/tmp" }
 | 
				
			||||||
func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
					func (s *stubNixOS) MustExecutable() string                   { return "/run/wrappers/bin/fortify" }
 | 
				
			||||||
func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
					func (s *stubNixOS) Exit(code int)                            { panic("called exit on stub with code " + strconv.Itoa(code)) }
 | 
				
			||||||
@ -54,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch file {
 | 
						switch file {
 | 
				
			||||||
	case "sudo":
 | 
						case "zsh":
 | 
				
			||||||
		return "/run/wrappers/bin/sudo", nil
 | 
							return "/run/current-system/sw/bin/zsh", nil
 | 
				
			||||||
	case "machinectl":
 | 
					 | 
				
			||||||
		return "/home/ophestra/.nix-profile/bin/machinectl", nil
 | 
					 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
							panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,19 +8,19 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type sealTestCase struct {
 | 
					type sealTestCase struct {
 | 
				
			||||||
	name      string
 | 
						name          string
 | 
				
			||||||
	os        sys.State
 | 
						os            sys.State
 | 
				
			||||||
	config    *fst.Config
 | 
						config        *fst.Config
 | 
				
			||||||
	id        fst.ID
 | 
						id            fst.ID
 | 
				
			||||||
	wantSys   *system.I
 | 
						wantSys       *system.I
 | 
				
			||||||
	wantBwrap *bwrap.Config
 | 
						wantContainer *sandbox.Params
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestApp(t *testing.T) {
 | 
					func TestApp(t *testing.T) {
 | 
				
			||||||
@ -30,15 +30,15 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			a := app.NewWithID(tc.id, tc.os)
 | 
								a := app.NewWithID(tc.id, tc.os)
 | 
				
			||||||
			var (
 | 
								var (
 | 
				
			||||||
				gotSys   *system.I
 | 
									gotSys       *system.I
 | 
				
			||||||
				gotBwrap *bwrap.Config
 | 
									gotContainer *sandbox.Params
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			if !t.Run("seal", func(t *testing.T) {
 | 
								if !t.Run("seal", func(t *testing.T) {
 | 
				
			||||||
				if sa, err := a.Seal(tc.config); err != nil {
 | 
									if sa, err := a.Seal(tc.config); err != nil {
 | 
				
			||||||
					t.Errorf("Seal: error = %v", err)
 | 
										t.Errorf("Seal: error = %v", err)
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
 | 
										gotSys, gotContainer = app.AppIParams(a, sa)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}) {
 | 
								}) {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			t.Run("compare bwrap", func(t *testing.T) {
 | 
								t.Run("compare params", func(t *testing.T) {
 | 
				
			||||||
				if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
 | 
									if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
 | 
				
			||||||
					t.Errorf("seal: bwrap =\n%s\n, want\n%s",
 | 
										t.Errorf("seal: params =\n%s\n, want\n%s",
 | 
				
			||||||
						mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
 | 
											mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,8 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
 | 
				
			|||||||
	return a
 | 
						return a
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
 | 
					func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
 | 
				
			||||||
	v := a.(*app)
 | 
						v := a.(*app)
 | 
				
			||||||
	seal := sa.(*outcome)
 | 
						seal := sa.(*outcome)
 | 
				
			||||||
	if v.outcome != seal || v.id != seal.id {
 | 
						if v.outcome != seal || v.id != seal.id {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// used by the parent process
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TryArgv0 calls [Main] if argv0 indicates the process is started from a file named "init".
 | 
					 | 
				
			||||||
func TryArgv0() {
 | 
					 | 
				
			||||||
	if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
 | 
					 | 
				
			||||||
		Main()
 | 
					 | 
				
			||||||
		internal.Exit(0)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,165 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	// time to wait for linger processes after death of initial process
 | 
					 | 
				
			||||||
	residualProcessTimeout = 5 * time.Second
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// everything beyond this point runs within pid namespace
 | 
					 | 
				
			||||||
// proceed with caution!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Main() {
 | 
					 | 
				
			||||||
	// sharing stdout with shim
 | 
					 | 
				
			||||||
	// USE WITH CAUTION
 | 
					 | 
				
			||||||
	fmsg.Prepare("init")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setting this prevents ptrace
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if os.Getpid() != 1 {
 | 
					 | 
				
			||||||
		log.Fatal("this process must run as pid 1")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// receive setup payload
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		payload    Payload
 | 
					 | 
				
			||||||
		closeSetup func() error
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if f, err := proc.Receive(Env, &payload); err != nil {
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrInvalid) {
 | 
					 | 
				
			||||||
			log.Fatal("invalid config descriptor")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrNotSet) {
 | 
					 | 
				
			||||||
			log.Fatal("FORTIFY_INIT not set")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Fatalf("cannot decode init setup payload: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmsg.Store(payload.Verbose)
 | 
					 | 
				
			||||||
		closeSetup = f
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// child does not need to see this
 | 
					 | 
				
			||||||
		if err = os.Unsetenv(Env); err != nil {
 | 
					 | 
				
			||||||
			log.Printf("cannot unset %s: %v", Env, err)
 | 
					 | 
				
			||||||
			// not fatal
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			fmsg.Verbose("received configuration")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// die with parent
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cmd := exec.Command(payload.Argv0)
 | 
					 | 
				
			||||||
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
					 | 
				
			||||||
	cmd.Args = payload.Argv
 | 
					 | 
				
			||||||
	cmd.Env = os.Environ()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := cmd.Start(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot start %q: %v", payload.Argv0, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fmsg.Suspend()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// close setup pipe as setup is now complete
 | 
					 | 
				
			||||||
	if err := closeSetup(); err != nil {
 | 
					 | 
				
			||||||
		log.Println("cannot close setup pipe:", err)
 | 
					 | 
				
			||||||
		// not fatal
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	sig := make(chan os.Signal, 2)
 | 
					 | 
				
			||||||
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	type winfo struct {
 | 
					 | 
				
			||||||
		wpid    int
 | 
					 | 
				
			||||||
		wstatus syscall.WaitStatus
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	info := make(chan winfo, 1)
 | 
					 | 
				
			||||||
	done := make(chan struct{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	go func() {
 | 
					 | 
				
			||||||
		var (
 | 
					 | 
				
			||||||
			err     error
 | 
					 | 
				
			||||||
			wpid    = -2
 | 
					 | 
				
			||||||
			wstatus syscall.WaitStatus
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// keep going until no child process is left
 | 
					 | 
				
			||||||
		for wpid != -1 {
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if wpid != -2 {
 | 
					 | 
				
			||||||
				info <- winfo{wpid, wstatus}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			err = syscall.EINTR
 | 
					 | 
				
			||||||
			for errors.Is(err, syscall.EINTR) {
 | 
					 | 
				
			||||||
				wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if !errors.Is(err, syscall.ECHILD) {
 | 
					 | 
				
			||||||
			log.Println("unexpected wait4 response:", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		close(done)
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// closed after residualProcessTimeout has elapsed after initial process death
 | 
					 | 
				
			||||||
	timeout := make(chan struct{})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	r := 2
 | 
					 | 
				
			||||||
	for {
 | 
					 | 
				
			||||||
		select {
 | 
					 | 
				
			||||||
		case s := <-sig:
 | 
					 | 
				
			||||||
			if fmsg.Resume() {
 | 
					 | 
				
			||||||
				fmsg.Verbosef("terminating on %s after process start", s.String())
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				fmsg.Verbosef("terminating on %s", s.String())
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			internal.Exit(0)
 | 
					 | 
				
			||||||
		case w := <-info:
 | 
					 | 
				
			||||||
			if w.wpid == cmd.Process.Pid {
 | 
					 | 
				
			||||||
				// initial process exited, output is most likely available again
 | 
					 | 
				
			||||||
				fmsg.Resume()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				switch {
 | 
					 | 
				
			||||||
				case w.wstatus.Exited():
 | 
					 | 
				
			||||||
					r = w.wstatus.ExitStatus()
 | 
					 | 
				
			||||||
				case w.wstatus.Signaled():
 | 
					 | 
				
			||||||
					r = 128 + int(w.wstatus.Signal())
 | 
					 | 
				
			||||||
				default:
 | 
					 | 
				
			||||||
					r = 255
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				go func() {
 | 
					 | 
				
			||||||
					time.Sleep(residualProcessTimeout)
 | 
					 | 
				
			||||||
					close(timeout)
 | 
					 | 
				
			||||||
				}()
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		case <-done:
 | 
					 | 
				
			||||||
			internal.Exit(r)
 | 
					 | 
				
			||||||
		case <-timeout:
 | 
					 | 
				
			||||||
			log.Println("timeout exceeded waiting for lingering processes")
 | 
					 | 
				
			||||||
			internal.Exit(r)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
package init0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_INIT"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// target full exec path
 | 
					 | 
				
			||||||
	Argv0 string
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -3,17 +3,12 @@ package app
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
@ -21,7 +16,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const shimSetupTimeout = 5 * time.Second
 | 
					const shimSetupTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
					func (seal *outcome) Run(rs *fst.RunState) error {
 | 
				
			||||||
	if !seal.f.CompareAndSwap(false, true) {
 | 
						if !seal.f.CompareAndSwap(false, true) {
 | 
				
			||||||
		// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
							// run does much more than just starting a process; calling it twice, even if the first call fails, will result
 | 
				
			||||||
		// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
							// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
 | 
				
			||||||
@ -37,33 +32,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)
 | 
				
			||||||
@ -79,17 +52,16 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
					revert app setup transaction
 | 
										revert app setup transaction
 | 
				
			||||||
				*/
 | 
									*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				rt, ec := new(system.Enablements), new(system.Criteria)
 | 
									var rt system.Enablement
 | 
				
			||||||
				ec.Enablements = new(system.Enablements)
 | 
									ec := system.Process
 | 
				
			||||||
				ec.Set(system.Process)
 | 
					 | 
				
			||||||
				if states, err := c.Load(); err != nil {
 | 
									if states, err := c.Load(); err != nil {
 | 
				
			||||||
					// revert per-process state here to limit damage
 | 
										// revert per-process state here to limit damage
 | 
				
			||||||
					storeErr.OpErr = err
 | 
										storeErr.OpErr = err
 | 
				
			||||||
					return seal.sys.Revert(ec)
 | 
										return seal.sys.Revert((*system.Criteria)(&ec))
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					if l := len(states); l == 0 {
 | 
										if l := len(states); l == 0 {
 | 
				
			||||||
						fmsg.Verbose("no other launchers active, will clean up globals")
 | 
											fmsg.Verbose("no other launchers active, will clean up globals")
 | 
				
			||||||
						ec.Set(system.User)
 | 
											ec |= system.User
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
 | 
											fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@ -97,31 +69,20 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
					// accumulate enablements of remaining launchers
 | 
										// accumulate enablements of remaining launchers
 | 
				
			||||||
					for i, s := range states {
 | 
										for i, s := range states {
 | 
				
			||||||
						if s.Config != nil {
 | 
											if s.Config != nil {
 | 
				
			||||||
							*rt |= s.Config.Confinement.Enablements
 | 
												rt |= s.Config.Confinement.Enablements
 | 
				
			||||||
						} else {
 | 
											} else {
 | 
				
			||||||
							log.Printf("state entry %d does not contain config", i)
 | 
												log.Printf("state entry %d does not contain config", i)
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// invert accumulated enablements for cleanup
 | 
									ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
 | 
				
			||||||
				for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
 | 
					 | 
				
			||||||
					if !rt.Has(i) {
 | 
					 | 
				
			||||||
						ec.Set(i)
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				if fmsg.Load() {
 | 
									if fmsg.Load() {
 | 
				
			||||||
					labels := make([]string, 0, system.ELen+1)
 | 
										if ec > 0 {
 | 
				
			||||||
					for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
 | 
											fmsg.Verbose("reverting operations type", system.TypeString(ec))
 | 
				
			||||||
						if ec.Has(i) {
 | 
					 | 
				
			||||||
							labels = append(labels, system.TypeString(i))
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if len(labels) > 0 {
 | 
					 | 
				
			||||||
						fmsg.Verbose("reverting operations type", strings.Join(labels, ", "))
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return seal.sys.Revert(ec)
 | 
									return seal.sys.Revert((*system.Criteria)(&ec))
 | 
				
			||||||
			}()
 | 
								}()
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		storeErr.save([]error{revertErr, store.Close()})
 | 
							storeErr.save([]error{revertErr, store.Close()})
 | 
				
			||||||
@ -133,11 +94,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	waitErr := make(chan error, 1)
 | 
						waitErr := make(chan error, 1)
 | 
				
			||||||
	cmd := new(shim.Shim)
 | 
						cmd := new(shimProcess)
 | 
				
			||||||
	if startTime, err := cmd.Start(
 | 
						if startTime, err := cmd.Start(
 | 
				
			||||||
		seal.user.aid.String(),
 | 
							seal.user.aid.String(),
 | 
				
			||||||
		seal.user.supp,
 | 
							seal.user.supp,
 | 
				
			||||||
		seal.bwrapSync,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -145,7 +105,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		rs.Time = startTime
 | 
							rs.Time = startTime
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
 | 
						ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
@ -154,11 +114,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
 | 
				
			|||||||
		cancel()
 | 
							cancel()
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := cmd.Serve(c, &shim.Payload{
 | 
						if err := cmd.Serve(ctx, &shimParams{
 | 
				
			||||||
		Argv:  seal.command,
 | 
							Container: seal.container,
 | 
				
			||||||
		Exec:  shimExec,
 | 
							Home:      seal.user.data,
 | 
				
			||||||
		Bwrap: seal.container,
 | 
					 | 
				
			||||||
		Home:  seal.user.data,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Verbose: fmsg.Load(),
 | 
							Verbose: fmsg.Load(),
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
@ -199,18 +157,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,26 +2,30 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/wl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/wl"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@ -65,19 +69,19 @@ type outcome struct {
 | 
				
			|||||||
	// copied from [sys.State] response
 | 
						// copied from [sys.State] response
 | 
				
			||||||
	runDirPath string
 | 
						runDirPath string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// passed through from [fst.Config]
 | 
					 | 
				
			||||||
	command []string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// initial [fst.Config] gob stream for state data;
 | 
						// initial [fst.Config] gob stream for state data;
 | 
				
			||||||
	// this is prepared ahead of time as config is mutated during seal creation
 | 
						// this is prepared ahead of time as config is clobbered during seal creation
 | 
				
			||||||
	ct io.WriterTo
 | 
						ct io.WriterTo
 | 
				
			||||||
	// dump dbus proxy message buffer
 | 
						// dump dbus proxy message buffer
 | 
				
			||||||
	dbusMsg func()
 | 
						dbusMsg func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user      fsuUser
 | 
						user fsuUser
 | 
				
			||||||
	sys       *system.I
 | 
						sys  *system.I
 | 
				
			||||||
	container *bwrap.Config
 | 
						ctx  context.Context
 | 
				
			||||||
	bwrapSync *os.File
 | 
					
 | 
				
			||||||
 | 
						container *sandbox.Params
 | 
				
			||||||
 | 
						env       map[string]string
 | 
				
			||||||
 | 
						sync      *os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	f atomic.Bool
 | 
						f atomic.Bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -100,7 +104,17 @@ type fsuUser struct {
 | 
				
			|||||||
	username string
 | 
						username string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
					func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
 | 
				
			||||||
 | 
						if seal.ctx != nil {
 | 
				
			||||||
 | 
							panic("finalise called twice")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						seal.ctx = ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shellPath := "/bin/sh"
 | 
				
			||||||
 | 
						if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
 | 
				
			||||||
 | 
							shellPath = s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// encode initial configuration for state tracking
 | 
							// encode initial configuration for state tracking
 | 
				
			||||||
		ct := new(bytes.Buffer)
 | 
							ct := new(bytes.Buffer)
 | 
				
			||||||
@ -111,9 +125,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.ct = ct
 | 
							seal.ct = ct
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass through command slice; this value is never touched in the main process
 | 
					 | 
				
			||||||
	seal.command = config.Command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// allowed aid range 0 to 9999, this is checked again in fsu
 | 
						// allowed aid range 0 to 9999, this is checked again in fsu
 | 
				
			||||||
	if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
						if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
 | 
				
			||||||
		return fmsg.WrapError(ErrUser,
 | 
							return fmsg.WrapError(ErrUser,
 | 
				
			||||||
@ -167,12 +178,24 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if config.Confinement.Sandbox == nil {
 | 
						if config.Confinement.Sandbox == nil {
 | 
				
			||||||
		fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
							fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fsu clears the environment so resolve paths early
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								if len(config.Args) > 0 {
 | 
				
			||||||
 | 
									if p, err := sys.LookPath(config.Args[0]); err != nil {
 | 
				
			||||||
 | 
										return fmsg.WrapError(err, err.Error())
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										config.Path = p
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									config.Path = shellPath
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf := &fst.SandboxConfig{
 | 
							conf := &fst.SandboxConfig{
 | 
				
			||||||
			UserNS:       true,
 | 
								Userns:  true,
 | 
				
			||||||
			Net:          true,
 | 
								Net:     true,
 | 
				
			||||||
			Syscall:      new(bwrap.SyscallPolicy),
 | 
								Tty:     true,
 | 
				
			||||||
			NoNewSession: true,
 | 
								AutoEtc: true,
 | 
				
			||||||
			AutoEtc:      true,
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind entries in /
 | 
							// bind entries in /
 | 
				
			||||||
		if d, err := sys.ReadDir("/"); err != nil {
 | 
							if d, err := sys.ReadDir("/"); err != nil {
 | 
				
			||||||
@ -198,10 +221,10 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		// hide nscd from sandbox if present
 | 
							// hide nscd from sandbox if present
 | 
				
			||||||
		nscd := "/var/run/nscd"
 | 
							nscd := "/var/run/nscd"
 | 
				
			||||||
		if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
							if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
			conf.Override = append(conf.Override, nscd)
 | 
								conf.Cover = append(conf.Cover, nscd)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// bind GPU stuff
 | 
							// bind GPU stuff
 | 
				
			||||||
		if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
 | 
							if config.Confinement.Enablements&(system.EX11|system.EWayland) != 0 {
 | 
				
			||||||
			conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
 | 
								conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// opportunistically bind kvm
 | 
							// opportunistically bind kvm
 | 
				
			||||||
@ -210,17 +233,29 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		config.Confinement.Sandbox = conf
 | 
							config.Confinement.Sandbox = conf
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var mapuid *stringPair[int]
 | 
						var mapuid, mapgid *stringPair[int]
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		var uid int
 | 
							var uid, gid int
 | 
				
			||||||
		var err error
 | 
							var err error
 | 
				
			||||||
		seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
 | 
							seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
									"cannot initialise container configuration:")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if !path.IsAbs(config.Path) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.EINVAL,
 | 
				
			||||||
 | 
									"invalid program path")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(config.Args) == 0 {
 | 
				
			||||||
 | 
								config.Args = []string{config.Path}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							seal.container.Path = config.Path
 | 
				
			||||||
 | 
							seal.container.Args = config.Args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mapuid = newInt(uid)
 | 
							mapuid = newInt(uid)
 | 
				
			||||||
		if seal.container.SetEnv == nil {
 | 
							mapgid = newInt(gid)
 | 
				
			||||||
			seal.container.SetEnv = make(map[string]string)
 | 
							if seal.env == nil {
 | 
				
			||||||
 | 
								seal.env = make(map[string]string)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -231,10 +266,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	sc := sys.Paths()
 | 
						sc := sys.Paths()
 | 
				
			||||||
	seal.runDirPath = sc.RunDirPath
 | 
						seal.runDirPath = sc.RunDirPath
 | 
				
			||||||
	seal.sys = system.New(seal.user.uid.unwrap())
 | 
						seal.sys = system.New(seal.user.uid.unwrap())
 | 
				
			||||||
	seal.sys.IsVerbose = fmsg.Load
 | 
					 | 
				
			||||||
	seal.sys.Verbose = fmsg.Verbose
 | 
					 | 
				
			||||||
	seal.sys.Verbosef = fmsg.Verbosef
 | 
					 | 
				
			||||||
	seal.sys.WrapErr = fmsg.WrapError
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Work directories
 | 
							Work directories
 | 
				
			||||||
@ -259,35 +290,27 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
						// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
 | 
				
			||||||
	innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
						innerRuntimeDir := path.Join("/run/user", mapuid.String())
 | 
				
			||||||
	seal.container.Tmpfs("/run/user", 1*1024*1024)
 | 
						seal.container.Tmpfs("/run/user", 1<<12, 0755)
 | 
				
			||||||
	seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
 | 
						seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
 | 
				
			||||||
	seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
 | 
						seal.env[xdgRuntimeDir] = innerRuntimeDir
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionClass] = "user"
 | 
						seal.env[xdgSessionClass] = "user"
 | 
				
			||||||
	seal.container.SetEnv[xdgSessionType] = "tty"
 | 
						seal.env[xdgSessionType] = "tty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// outer path for inner /tmp
 | 
						// outer path for inner /tmp
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
							tmpdir := path.Join(sc.SharePath, "tmpdir")
 | 
				
			||||||
		seal.sys.Ensure(tmpdir, 0700)
 | 
							seal.sys.Ensure(tmpdir, 0700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
 | 
				
			||||||
		tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
 | 
							tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
 | 
				
			||||||
		seal.sys.Ensure(tmpdirProc, 01700)
 | 
							seal.sys.Ensure(tmpdirInst, 01700)
 | 
				
			||||||
		seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
 | 
							seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		seal.container.Bind(tmpdirProc, "/tmp", false, true)
 | 
							seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Passwd database
 | 
							Passwd database
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// look up shell
 | 
					 | 
				
			||||||
	sh := "/bin/sh"
 | 
					 | 
				
			||||||
	if s, ok := sys.LookupEnv(shell); ok {
 | 
					 | 
				
			||||||
		seal.container.SetEnv[shell] = s
 | 
					 | 
				
			||||||
		sh = s
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// bind home directory
 | 
					 | 
				
			||||||
	homeDir := "/var/empty"
 | 
						homeDir := "/var/empty"
 | 
				
			||||||
	if seal.user.home != "" {
 | 
						if seal.user.home != "" {
 | 
				
			||||||
		homeDir = seal.user.home
 | 
							homeDir = seal.user.home
 | 
				
			||||||
@ -296,28 +319,26 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
	if seal.user.username != "" {
 | 
						if seal.user.username != "" {
 | 
				
			||||||
		username = seal.user.username
 | 
							username = seal.user.username
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	seal.container.Bind(seal.user.data, homeDir, false, true)
 | 
						seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
 | 
				
			||||||
	seal.container.Chdir = homeDir
 | 
						seal.container.Dir = homeDir
 | 
				
			||||||
	seal.container.SetEnv["HOME"] = homeDir
 | 
						seal.env["HOME"] = homeDir
 | 
				
			||||||
	seal.container.SetEnv["USER"] = username
 | 
						seal.env["USER"] = username
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// generate /etc/passwd and /etc/group
 | 
						seal.container.Place("/etc/passwd",
 | 
				
			||||||
	seal.container.CopyBind("/etc/passwd",
 | 
							[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
 | 
				
			||||||
		[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
 | 
						seal.container.Place("/etc/group",
 | 
				
			||||||
	seal.container.CopyBind("/etc/group",
 | 
							[]byte("fortify:x:"+mapgid.String()+":\n"))
 | 
				
			||||||
		[]byte("fortify:x:"+mapuid.String()+":\n"))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
						/*
 | 
				
			||||||
		Display servers
 | 
							Display servers
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass $TERM to launcher
 | 
						// pass $TERM for proper terminal I/O in shell
 | 
				
			||||||
	if t, ok := sys.LookupEnv(term); ok {
 | 
						if t, ok := sys.LookupEnv(term); ok {
 | 
				
			||||||
		seal.container.SetEnv[term] = t
 | 
							seal.env[term] = t
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up wayland
 | 
						if config.Confinement.Enablements&system.EWayland != 0 {
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EWayland) {
 | 
					 | 
				
			||||||
		// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
							// outer wayland socket (usually `/run/user/%d/wayland-%d`)
 | 
				
			||||||
		var socketPath string
 | 
							var socketPath string
 | 
				
			||||||
		if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
 | 
							if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
 | 
				
			||||||
@ -330,7 +351,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
							innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
 | 
				
			||||||
		seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
 | 
							seal.env[wl.WaylandDisplay] = wl.FallbackName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
							if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
 | 
				
			||||||
			socketDir := path.Join(sc.SharePath, "wayland")
 | 
								socketDir := path.Join(sc.SharePath, "wayland")
 | 
				
			||||||
@ -341,25 +362,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
				// use instance ID in case app id is not set
 | 
									// use instance ID in case app id is not set
 | 
				
			||||||
				appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
									appID = "uk.gensokyo.fortify." + seal.id.String()
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
 | 
								seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
 | 
				
			||||||
			seal.container.Bind(outerPath, innerPath)
 | 
								seal.container.Bind(outerPath, innerPath, 0)
 | 
				
			||||||
		} else { // bind mount wayland socket (insecure)
 | 
							} else { // bind mount wayland socket (insecure)
 | 
				
			||||||
			fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
								fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
 | 
				
			||||||
			seal.container.Bind(socketPath, innerPath)
 | 
								seal.container.Bind(socketPath, innerPath, 0)
 | 
				
			||||||
			seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
								seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up X11
 | 
						if config.Confinement.Enablements&system.EX11 != 0 {
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EX11) {
 | 
					 | 
				
			||||||
		// discover X11 and grant user permission via the `ChangeHosts` command
 | 
					 | 
				
			||||||
		if d, ok := sys.LookupEnv(display); !ok {
 | 
							if d, ok := sys.LookupEnv(display); !ok {
 | 
				
			||||||
			return fmsg.WrapError(ErrXDisplay,
 | 
								return fmsg.WrapError(ErrXDisplay,
 | 
				
			||||||
				"DISPLAY is not set")
 | 
									"DISPLAY is not set")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
								seal.sys.ChangeHosts("#" + seal.user.uid.String())
 | 
				
			||||||
			seal.container.SetEnv[display] = d
 | 
								seal.env[display] = d
 | 
				
			||||||
			seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
 | 
								seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -367,7 +386,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		PulseAudio server and authentication
 | 
							PulseAudio server and authentication
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EPulse) {
 | 
						if config.Confinement.Enablements&system.EPulse != 0 {
 | 
				
			||||||
		// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
 | 
							// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
 | 
				
			||||||
		pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
 | 
							pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
 | 
				
			||||||
		// PulseAudio socket (usually `/run/user/%d/pulse/native`)
 | 
							// PulseAudio socket (usually `/run/user/%d/pulse/native`)
 | 
				
			||||||
@ -400,8 +419,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
							innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
 | 
				
			||||||
		innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
							innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
 | 
				
			||||||
		seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
							seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
 | 
				
			||||||
		seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
 | 
							seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
 | 
				
			||||||
		seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
 | 
							seal.env[pulseServer] = "unix:" + innerPulseSocket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// publish current user's pulse cookie for target user
 | 
							// publish current user's pulse cookie for target user
 | 
				
			||||||
		if src, err := discoverPulseCookie(sys); err != nil {
 | 
							if src, err := discoverPulseCookie(sys); err != nil {
 | 
				
			||||||
@ -409,9 +428,9 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
			fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
								fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
								innerDst := fst.Tmp + "/pulse-cookie"
 | 
				
			||||||
			seal.container.SetEnv[pulseCookie] = innerDst
 | 
								seal.env[pulseCookie] = innerDst
 | 
				
			||||||
			payload := new([]byte)
 | 
								var payload *[]byte
 | 
				
			||||||
			seal.container.CopyBindRef(innerDst, &payload)
 | 
								seal.container.PlaceP(innerDst, &payload)
 | 
				
			||||||
			seal.sys.CopyFile(payload, src, 256, 256)
 | 
								seal.sys.CopyFile(payload, src, 256, 256)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -420,7 +439,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		D-Bus proxy
 | 
							D-Bus proxy
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if config.Confinement.Enablements.Has(system.EDBus) {
 | 
						if config.Confinement.Enablements&system.EDBus != 0 {
 | 
				
			||||||
		// ensure dbus session bus defaults
 | 
							// ensure dbus session bus defaults
 | 
				
			||||||
		if config.Confinement.SessionBus == nil {
 | 
							if config.Confinement.SessionBus == nil {
 | 
				
			||||||
			config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
 | 
								config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
 | 
				
			||||||
@ -441,13 +460,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// share proxy sockets
 | 
							// share proxy sockets
 | 
				
			||||||
		sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
							sessionInner := path.Join(innerRuntimeDir, "bus")
 | 
				
			||||||
		seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
							seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
 | 
				
			||||||
		seal.container.Bind(sessionPath, sessionInner)
 | 
							seal.container.Bind(sessionPath, sessionInner, 0)
 | 
				
			||||||
		seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
							seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
 | 
				
			||||||
		if config.Confinement.SystemBus != nil {
 | 
							if config.Confinement.SystemBus != nil {
 | 
				
			||||||
			systemInner := "/run/dbus/system_bus_socket"
 | 
								systemInner := "/run/dbus/system_bus_socket"
 | 
				
			||||||
			seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
								seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
 | 
				
			||||||
			seal.container.Bind(systemPath, systemInner)
 | 
								seal.container.Bind(systemPath, systemInner, 0)
 | 
				
			||||||
			seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
								seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -456,9 +475,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		Miscellaneous
 | 
							Miscellaneous
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// queue overriding tmpfs at the end of seal.container.Filesystem
 | 
						for _, dest := range config.Confinement.Sandbox.Cover {
 | 
				
			||||||
	for _, dest := range config.Confinement.Sandbox.Override {
 | 
							seal.container.Tmpfs(dest, 1<<13, 0755)
 | 
				
			||||||
		seal.container.Tmpfs(dest, 8*1024)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// append ExtraPerms last
 | 
						// append ExtraPerms last
 | 
				
			||||||
@ -484,12 +502,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
 | 
				
			|||||||
		seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
							seal.sys.UpdatePermType(system.User, p.Path, perms...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// mount fortify in sandbox for init
 | 
						// flatten and sort env for deterministic behaviour
 | 
				
			||||||
	seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
 | 
						seal.container.Env = make([]string, 0, len(seal.env))
 | 
				
			||||||
	seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
 | 
						maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
 | 
				
			||||||
 | 
						slices.Sort(seal.container.Env)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
 | 
						fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
 | 
				
			||||||
		seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
 | 
							seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										212
									
								
								internal/app/shim.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								internal/app/shim.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,212 @@
 | 
				
			|||||||
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const shimEnv = "FORTIFY_SHIM"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type shimParams struct {
 | 
				
			||||||
 | 
						// finalised container params
 | 
				
			||||||
 | 
						Container *sandbox.Params
 | 
				
			||||||
 | 
						// path to outer home directory
 | 
				
			||||||
 | 
						Home string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verbosity pass through
 | 
				
			||||||
 | 
						Verbose bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ShimMain is the main function of the shim process and runs as the unconstrained target user.
 | 
				
			||||||
 | 
					func ShimMain() {
 | 
				
			||||||
 | 
						fmsg.Prepare("shim")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							params     shimParams
 | 
				
			||||||
 | 
							closeSetup func() error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if f, err := sandbox.Receive(shimEnv, ¶ms, nil); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, sandbox.ErrInvalid) {
 | 
				
			||||||
 | 
								log.Fatal("invalid config descriptor")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, sandbox.ErrNotSet) {
 | 
				
			||||||
 | 
								log.Fatal("FORTIFY_SHIM not set")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Fatalf("cannot receive shim setup params: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							internal.InstallFmsg(params.Verbose)
 | 
				
			||||||
 | 
							closeSetup = f
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if params.Container == nil || params.Container.Ops == nil {
 | 
				
			||||||
 | 
							log.Fatal("invalid container params")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// close setup socket
 | 
				
			||||||
 | 
						if err := closeSetup(); err != nil {
 | 
				
			||||||
 | 
							log.Printf("cannot close setup pipe: %v", err)
 | 
				
			||||||
 | 
							// not fatal
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ensure home directory as target user
 | 
				
			||||||
 | 
						if s, err := os.Stat(params.Home); err != nil {
 | 
				
			||||||
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
 | 
								if err = os.Mkdir(params.Home, 0700); err != nil {
 | 
				
			||||||
 | 
									log.Fatalf("cannot create home directory: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								log.Fatalf("cannot access home directory: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// home directory is created, proceed
 | 
				
			||||||
 | 
						} else if !s.IsDir() {
 | 
				
			||||||
 | 
							log.Fatalf("path %q is not a directory", params.Home)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var name string
 | 
				
			||||||
 | 
						if len(params.Container.Args) > 0 {
 | 
				
			||||||
 | 
							name = params.Container.Args[0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
				
			||||||
 | 
						defer stop() // unreachable
 | 
				
			||||||
 | 
						container := sandbox.New(ctx, name)
 | 
				
			||||||
 | 
						container.Params = *params.Container
 | 
				
			||||||
 | 
						container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
 | 
				
			||||||
 | 
						container.WaitDelay = 2 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := container.Start(); err != nil {
 | 
				
			||||||
 | 
							fmsg.PrintBaseError(err, "cannot start container:")
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Serve(); err != nil {
 | 
				
			||||||
 | 
							fmsg.PrintBaseError(err, "cannot configure container:")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Wait(); err != nil {
 | 
				
			||||||
 | 
							var exitError *exec.ExitError
 | 
				
			||||||
 | 
							if !errors.As(err, &exitError) {
 | 
				
			||||||
 | 
								if errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
									os.Exit(2)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								log.Printf("wait: %v", err)
 | 
				
			||||||
 | 
								os.Exit(127)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							os.Exit(exitError.ExitCode())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type shimProcess struct {
 | 
				
			||||||
 | 
						// user switcher process
 | 
				
			||||||
 | 
						cmd *exec.Cmd
 | 
				
			||||||
 | 
						// fallback exit notifier with error returned killing the process
 | 
				
			||||||
 | 
						killFallback chan error
 | 
				
			||||||
 | 
						// monitor to shim encoder
 | 
				
			||||||
 | 
						encoder *gob.Encoder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Unwrap() *exec.Cmd    { return s.cmd }
 | 
				
			||||||
 | 
					func (s *shimProcess) Fallback() chan error { return s.killFallback }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) String() string {
 | 
				
			||||||
 | 
						if s.cmd == nil {
 | 
				
			||||||
 | 
							return "(unused shim manager)"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.cmd.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Start(
 | 
				
			||||||
 | 
						aid string,
 | 
				
			||||||
 | 
						supp []string,
 | 
				
			||||||
 | 
					) (*time.Time, error) {
 | 
				
			||||||
 | 
						// prepare user switcher invocation
 | 
				
			||||||
 | 
						fsuPath := internal.MustFsuPath()
 | 
				
			||||||
 | 
						s.cmd = exec.Command(fsuPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// pass shim setup pipe
 | 
				
			||||||
 | 
						if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
				
			||||||
 | 
							return nil, fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
								"cannot create shim setup pipe:")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							s.encoder = e
 | 
				
			||||||
 | 
							s.cmd.Env = []string{
 | 
				
			||||||
 | 
								shimEnv + "=" + strconv.Itoa(fd),
 | 
				
			||||||
 | 
								"FORTIFY_APP_ID=" + aid,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// format fsu supplementary groups
 | 
				
			||||||
 | 
						if len(supp) > 0 {
 | 
				
			||||||
 | 
							fmsg.Verbosef("attaching supplementary group ids %s", supp)
 | 
				
			||||||
 | 
							s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						s.cmd.Dir = "/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
				
			||||||
 | 
						// withhold messages to stderr
 | 
				
			||||||
 | 
						fmsg.Suspend()
 | 
				
			||||||
 | 
						if err := s.cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							return nil, fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
								"cannot start fsu:")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						startTime := time.Now().UTC()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &startTime, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *shimProcess) Serve(ctx context.Context, params *shimParams) error {
 | 
				
			||||||
 | 
						// kill shim if something goes wrong and an error is returned
 | 
				
			||||||
 | 
						s.killFallback = make(chan error, 1)
 | 
				
			||||||
 | 
						killShim := func() {
 | 
				
			||||||
 | 
							if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
 | 
				
			||||||
 | 
								s.killFallback <- err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() { killShim() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encodeErr := make(chan error)
 | 
				
			||||||
 | 
						go func() { encodeErr <- s.encoder.Encode(params) }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						// encode return indicates setup completion
 | 
				
			||||||
 | 
						case err := <-encodeErr:
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
 | 
									"cannot transmit shim config:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							killShim = func() {}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// setup canceled before payload was accepted
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							err := ctx.Err()
 | 
				
			||||||
 | 
							if errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.ECANCELED,
 | 
				
			||||||
 | 
									"shim setup canceled")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, context.DeadlineExceeded) {
 | 
				
			||||||
 | 
								return fmsg.WrapError(syscall.ETIMEDOUT,
 | 
				
			||||||
 | 
									"deadline exceeded waiting for shim")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// unreachable
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,153 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	init0 "git.gensokyo.uk/security/fortify/internal/app/init"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// everything beyond this point runs as unconstrained target user
 | 
					 | 
				
			||||||
// proceed with caution!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Main() {
 | 
					 | 
				
			||||||
	// sharing stdout with fortify
 | 
					 | 
				
			||||||
	// USE WITH CAUTION
 | 
					 | 
				
			||||||
	fmsg.Prepare("shim")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setting this prevents ptrace
 | 
					 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// receive setup payload
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		payload    Payload
 | 
					 | 
				
			||||||
		closeSetup func() error
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if f, err := proc.Receive(Env, &payload); err != nil {
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrInvalid) {
 | 
					 | 
				
			||||||
			log.Fatal("invalid config descriptor")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, proc.ErrNotSet) {
 | 
					 | 
				
			||||||
			log.Fatal("FORTIFY_SHIM not set")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Fatalf("cannot decode shim setup payload: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmsg.Store(payload.Verbose)
 | 
					 | 
				
			||||||
		closeSetup = f
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if payload.Bwrap == nil {
 | 
					 | 
				
			||||||
		log.Fatal("bwrap config not supplied")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// restore bwrap sync fd
 | 
					 | 
				
			||||||
	var syncFd *os.File
 | 
					 | 
				
			||||||
	if payload.Sync != nil {
 | 
					 | 
				
			||||||
		syncFd = os.NewFile(*payload.Sync, "sync")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// close setup socket
 | 
					 | 
				
			||||||
	if err := closeSetup(); err != nil {
 | 
					 | 
				
			||||||
		log.Println("cannot close setup pipe:", err)
 | 
					 | 
				
			||||||
		// not fatal
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ensure home directory as target user
 | 
					 | 
				
			||||||
	if s, err := os.Stat(payload.Home); err != nil {
 | 
					 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
					 | 
				
			||||||
			if err = os.Mkdir(payload.Home, 0700); err != nil {
 | 
					 | 
				
			||||||
				log.Fatalf("cannot create home directory: %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			log.Fatalf("cannot access home directory: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// home directory is created, proceed
 | 
					 | 
				
			||||||
	} else if !s.IsDir() {
 | 
					 | 
				
			||||||
		log.Fatalf("data path %q is not a directory", payload.Home)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var ic init0.Payload
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// resolve argv0
 | 
					 | 
				
			||||||
	ic.Argv = payload.Argv
 | 
					 | 
				
			||||||
	if len(ic.Argv) > 0 {
 | 
					 | 
				
			||||||
		// looked up from $PATH by parent
 | 
					 | 
				
			||||||
		ic.Argv0 = payload.Exec[1]
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// no argv, look up shell instead
 | 
					 | 
				
			||||||
		var ok bool
 | 
					 | 
				
			||||||
		if payload.Bwrap.SetEnv == nil {
 | 
					 | 
				
			||||||
			log.Fatal("no command was specified and environment is unset")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
 | 
					 | 
				
			||||||
			log.Fatal("no command was specified and $SHELL was unset")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ic.Argv = []string{ic.Argv0}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conf := payload.Bwrap
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var extraFiles []*os.File
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// serve setup payload
 | 
					 | 
				
			||||||
	if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("cannot pipe: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		conf.SetEnv[init0.Env] = strconv.Itoa(fd)
 | 
					 | 
				
			||||||
		go func() {
 | 
					 | 
				
			||||||
			fmsg.Verbose("transmitting config to init")
 | 
					 | 
				
			||||||
			if err = encoder.Encode(&ic); err != nil {
 | 
					 | 
				
			||||||
				log.Fatalf("cannot transmit init config: %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
					 | 
				
			||||||
	if fmsg.Load() {
 | 
					 | 
				
			||||||
		seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if b, err := helper.NewBwrap(
 | 
					 | 
				
			||||||
		conf, path.Join(fst.Tmp, "sbin/init"), false,
 | 
					 | 
				
			||||||
		nil, func(int, int) []string { return make([]string, 0) },
 | 
					 | 
				
			||||||
		extraFiles,
 | 
					 | 
				
			||||||
		syncFd,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		log.Fatalf("malformed sandbox config: %v", err)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(os.Stderr)
 | 
					 | 
				
			||||||
		ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
					 | 
				
			||||||
		defer stop() // unreachable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// run and pass through exit code
 | 
					 | 
				
			||||||
		if err = b.Start(ctx, false); err != nil {
 | 
					 | 
				
			||||||
			log.Fatalf("cannot start target process: %v", err)
 | 
					 | 
				
			||||||
		} else if err = b.Wait(); err != nil {
 | 
					 | 
				
			||||||
			var exitError *exec.ExitError
 | 
					 | 
				
			||||||
			if !errors.As(err, &exitError) {
 | 
					 | 
				
			||||||
				log.Printf("wait: %v", err)
 | 
					 | 
				
			||||||
				internal.Exit(127)
 | 
					 | 
				
			||||||
				panic("unreachable")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			internal.Exit(exitError.ExitCode())
 | 
					 | 
				
			||||||
			panic("unreachable")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,133 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"encoding/gob"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/proc"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// used by the parent process
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Shim struct {
 | 
					 | 
				
			||||||
	// user switcher process
 | 
					 | 
				
			||||||
	cmd *exec.Cmd
 | 
					 | 
				
			||||||
	// fallback exit notifier with error returned killing the process
 | 
					 | 
				
			||||||
	killFallback chan error
 | 
					 | 
				
			||||||
	// monitor to shim encoder
 | 
					 | 
				
			||||||
	encoder *gob.Encoder
 | 
					 | 
				
			||||||
	// bwrap --sync-fd value
 | 
					 | 
				
			||||||
	sync *uintptr
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) String() string {
 | 
					 | 
				
			||||||
	if s.cmd == nil {
 | 
					 | 
				
			||||||
		return "(unused shim manager)"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return s.cmd.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Unwrap() *exec.Cmd {
 | 
					 | 
				
			||||||
	return s.cmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) WaitFallback() chan error {
 | 
					 | 
				
			||||||
	return s.killFallback
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Start(
 | 
					 | 
				
			||||||
	// string representation of application id
 | 
					 | 
				
			||||||
	aid string,
 | 
					 | 
				
			||||||
	// string representation of supplementary group ids
 | 
					 | 
				
			||||||
	supp []string,
 | 
					 | 
				
			||||||
	// bwrap --sync-fd
 | 
					 | 
				
			||||||
	syncFd *os.File,
 | 
					 | 
				
			||||||
) (*time.Time, error) {
 | 
					 | 
				
			||||||
	// prepare user switcher invocation
 | 
					 | 
				
			||||||
	fsuPath := internal.MustFsuPath()
 | 
					 | 
				
			||||||
	s.cmd = exec.Command(fsuPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// pass shim setup pipe
 | 
					 | 
				
			||||||
	if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
 | 
					 | 
				
			||||||
		return nil, fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
			"cannot create shim setup pipe:")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		s.encoder = e
 | 
					 | 
				
			||||||
		s.cmd.Env = []string{
 | 
					 | 
				
			||||||
			Env + "=" + strconv.Itoa(fd),
 | 
					 | 
				
			||||||
			"FORTIFY_APP_ID=" + aid,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// format fsu supplementary groups
 | 
					 | 
				
			||||||
	if len(supp) > 0 {
 | 
					 | 
				
			||||||
		fmsg.Verbosef("attaching supplementary group ids %s", supp)
 | 
					 | 
				
			||||||
		s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
					 | 
				
			||||||
	s.cmd.Dir = "/"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// pass sync fd if set
 | 
					 | 
				
			||||||
	if syncFd != nil {
 | 
					 | 
				
			||||||
		fd := proc.ExtraFile(s.cmd, syncFd)
 | 
					 | 
				
			||||||
		s.sync = &fd
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	fmsg.Verbose("starting shim via fsu:", s.cmd)
 | 
					 | 
				
			||||||
	// withhold messages to stderr
 | 
					 | 
				
			||||||
	fmsg.Suspend()
 | 
					 | 
				
			||||||
	if err := s.cmd.Start(); err != nil {
 | 
					 | 
				
			||||||
		return nil, fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
			"cannot start fsu:")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	startTime := time.Now().UTC()
 | 
					 | 
				
			||||||
	return &startTime, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
					 | 
				
			||||||
	// kill shim if something goes wrong and an error is returned
 | 
					 | 
				
			||||||
	s.killFallback = make(chan error, 1)
 | 
					 | 
				
			||||||
	killShim := func() {
 | 
					 | 
				
			||||||
		if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
 | 
					 | 
				
			||||||
			s.killFallback <- err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer func() { killShim() }()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	payload.Sync = s.sync
 | 
					 | 
				
			||||||
	encodeErr := make(chan error)
 | 
					 | 
				
			||||||
	go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	select {
 | 
					 | 
				
			||||||
	// encode return indicates setup completion
 | 
					 | 
				
			||||||
	case err := <-encodeErr:
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return fmsg.WrapErrorSuffix(err,
 | 
					 | 
				
			||||||
				"cannot transmit shim config:")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		killShim = func() {}
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// setup canceled before payload was accepted
 | 
					 | 
				
			||||||
	case <-ctx.Done():
 | 
					 | 
				
			||||||
		err := ctx.Err()
 | 
					 | 
				
			||||||
		if errors.Is(err, context.Canceled) {
 | 
					 | 
				
			||||||
			return fmsg.WrapError(errors.New("shim setup canceled"),
 | 
					 | 
				
			||||||
				"shim setup canceled")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if errors.Is(err, context.DeadlineExceeded) {
 | 
					 | 
				
			||||||
			return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
 | 
					 | 
				
			||||||
				"deadline exceeded waiting for shim")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// unreachable
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,23 +0,0 @@
 | 
				
			|||||||
package shim
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Env = "FORTIFY_SHIM"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Payload struct {
 | 
					 | 
				
			||||||
	// child full argv
 | 
					 | 
				
			||||||
	Argv []string
 | 
					 | 
				
			||||||
	// bwrap, target full exec path
 | 
					 | 
				
			||||||
	Exec [2]string
 | 
					 | 
				
			||||||
	// bwrap config
 | 
					 | 
				
			||||||
	Bwrap *bwrap.Config
 | 
					 | 
				
			||||||
	// path to outer home directory
 | 
					 | 
				
			||||||
	Home string
 | 
					 | 
				
			||||||
	// sync fd
 | 
					 | 
				
			||||||
	Sync *uintptr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// verbosity pass through
 | 
					 | 
				
			||||||
	Verbose bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								internal/fmsg/msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/fmsg/msg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					package fmsg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Output struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (Output) IsVerbose() bool                         { return Load() }
 | 
				
			||||||
 | 
					func (Output) Verbose(v ...any)                        { Verbose(v...) }
 | 
				
			||||||
 | 
					func (Output) Verbosef(format string, v ...any)        { Verbosef(format, v...) }
 | 
				
			||||||
 | 
					func (Output) WrapErr(err error, a ...any) error       { return WrapError(err, a...) }
 | 
				
			||||||
 | 
					func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
 | 
				
			||||||
 | 
					func (Output) Suspend()                                { Suspend() }
 | 
				
			||||||
 | 
					func (Output) Resume() bool                            { return Resume() }
 | 
				
			||||||
 | 
					func (Output) BeforeExit()                             { BeforeExit() }
 | 
				
			||||||
							
								
								
									
										17
									
								
								internal/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								internal/output.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func InstallFmsg(verbose bool) {
 | 
				
			||||||
 | 
						fmsg.Store(verbose)
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						system.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						if verbose {
 | 
				
			||||||
 | 
							seccomp.SetOutput(fmsg.Verbose)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
package internal
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "syscall"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
 | 
					 | 
				
			||||||
	// linux/sched/coredump.h
 | 
					 | 
				
			||||||
	if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
 | 
					 | 
				
			||||||
		return errno
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func PR_SET_PDEATHSIG__SIGKILL() error {
 | 
					 | 
				
			||||||
	if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
 | 
					 | 
				
			||||||
		return errno
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			slices.Sort(aids)
 | 
								slices.Sort(aids)
 | 
				
			||||||
			want := []int{0, 1}
 | 
								want := []int{0, 1}
 | 
				
			||||||
			if slices.Compare(aids, want) != 0 {
 | 
								if !slices.Equal(aids, want) {
 | 
				
			||||||
				t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
									t.Fatalf("List() = %#v, want %#v", aids, want)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,10 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// State provides safe interaction with operating system state.
 | 
					// State provides safe interaction with operating system state.
 | 
				
			||||||
type State interface {
 | 
					type State interface {
 | 
				
			||||||
	// Geteuid provides [os.Geteuid].
 | 
						// Getuid provides [os.Getuid].
 | 
				
			||||||
	Geteuid() int
 | 
						Getuid() int
 | 
				
			||||||
 | 
						// Getgid provides [os.Getgid].
 | 
				
			||||||
 | 
						Getgid() int
 | 
				
			||||||
	// LookupEnv provides [os.LookupEnv].
 | 
						// LookupEnv provides [os.LookupEnv].
 | 
				
			||||||
	LookupEnv(key string) (string, bool)
 | 
						LookupEnv(key string) (string, bool)
 | 
				
			||||||
	// TempDir provides [os.TempDir].
 | 
						// TempDir provides [os.TempDir].
 | 
				
			||||||
@ -47,7 +49,7 @@ type State interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CopyPaths is a generic implementation of [System.Paths].
 | 
					// CopyPaths is a generic implementation of [System.Paths].
 | 
				
			||||||
func CopyPaths(os State, v *fst.Paths) {
 | 
					func CopyPaths(os State, v *fst.Paths) {
 | 
				
			||||||
	v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
 | 
						v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
						fmsg.Verbosef("process share directory at %q", v.SharePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Std implements System using the standard library.
 | 
					// Std implements System using the standard library.
 | 
				
			||||||
@ -30,11 +31,12 @@ type Std struct {
 | 
				
			|||||||
	uidMu sync.RWMutex
 | 
						uidMu sync.RWMutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Std) Geteuid() int                                 { return os.Geteuid() }
 | 
					func (s *Std) Getuid() int                                  { return os.Getuid() }
 | 
				
			||||||
 | 
					func (s *Std) Getgid() int                                  { return os.Getgid() }
 | 
				
			||||||
func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
					func (s *Std) LookupEnv(key string) (string, bool)          { return os.LookupEnv(key) }
 | 
				
			||||||
func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
					func (s *Std) TempDir() string                              { return os.TempDir() }
 | 
				
			||||||
func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
					func (s *Std) LookPath(file string) (string, error)         { return exec.LookPath(file) }
 | 
				
			||||||
func (s *Std) MustExecutable() string                       { return internal.MustExecutable() }
 | 
					func (s *Std) MustExecutable() string                       { return sandbox.MustExecutable() }
 | 
				
			||||||
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
 | 
					func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
 | 
				
			||||||
func (s *Std) ReadDir(name string) ([]os.DirEntry, error)   { return os.ReadDir(name) }
 | 
					func (s *Std) ReadDir(name string) ([]os.DirEntry, error)   { return os.ReadDir(name) }
 | 
				
			||||||
func (s *Std) Stat(name string) (fs.FileInfo, error)        { return os.Stat(name) }
 | 
					func (s *Std) Stat(name string) (fs.FileInfo, error)        { return os.Stat(name) }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										58
									
								
								ldd/exec.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								ldd/exec.go
									
									
									
									
									
								
							@ -3,56 +3,56 @@ package ldd
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const lddTimeout = 2 * time.Second
 | 
					const lddTimeout = 2 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
 | 
						msgStatic      = []byte("Not a valid dynamic program")
 | 
				
			||||||
	msgStaticGlibc = []byte("not a dynamic executable")
 | 
						msgStaticGlibc = []byte("not a dynamic executable")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Exec(ctx context.Context, p string) ([]*Entry, error) {
 | 
					func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
 | 
				
			||||||
	var h helper.Helper
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if toolPath, err := exec.LookPath("ldd"); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	} else if h, err = helper.NewBwrap(
 | 
					 | 
				
			||||||
		(&bwrap.Config{
 | 
					 | 
				
			||||||
			Hostname:      "fortify-ldd",
 | 
					 | 
				
			||||||
			Chdir:         "/",
 | 
					 | 
				
			||||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
					 | 
				
			||||||
			NewSession:    true,
 | 
					 | 
				
			||||||
			DieWithParent: true,
 | 
					 | 
				
			||||||
		}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
 | 
					 | 
				
			||||||
		nil, func(_, _ int) []string { return []string{p} },
 | 
					 | 
				
			||||||
		nil, nil,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
 | 
					 | 
				
			||||||
	h.Stdout(stdout).Stderr(stderr)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ExecFilter(ctx context.Context,
 | 
				
			||||||
 | 
						commandContext func(context.Context) *exec.Cmd,
 | 
				
			||||||
 | 
						f func([]byte) []byte,
 | 
				
			||||||
 | 
						p string) ([]*Entry, error) {
 | 
				
			||||||
	c, cancel := context.WithTimeout(ctx, lddTimeout)
 | 
						c, cancel := context.WithTimeout(ctx, lddTimeout)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
	if err := h.Start(c, false); err != nil {
 | 
						container := sandbox.New(c, "ldd", p)
 | 
				
			||||||
 | 
						container.CommandContext = commandContext
 | 
				
			||||||
 | 
						container.Hostname = "fortify-ldd"
 | 
				
			||||||
 | 
						stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
 | 
				
			||||||
 | 
						container.Stdout = stdout
 | 
				
			||||||
 | 
						container.Stderr = stderr
 | 
				
			||||||
 | 
						container.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := container.Start(); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := h.Wait(); err != nil {
 | 
						defer func() { _, _ = io.Copy(os.Stderr, stderr) }()
 | 
				
			||||||
 | 
						if err := container.Serve(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := container.Wait(); err != nil {
 | 
				
			||||||
		m := stderr.Bytes()
 | 
							m := stderr.Bytes()
 | 
				
			||||||
		if bytes.Contains(m, msgStaticGlibc) {
 | 
							if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) ||
 | 
				
			||||||
 | 
								bytes.Contains(m, msgStaticGlibc) {
 | 
				
			||||||
			return nil, nil
 | 
								return nil, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		_, _ = os.Stderr.Write(m)
 | 
					 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return Parse(stdout)
 | 
						v := stdout.Bytes()
 | 
				
			||||||
 | 
						if f != nil {
 | 
				
			||||||
 | 
							v = f(v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Parse(v)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
package ldd
 | 
					package ldd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@ -15,8 +14,8 @@ type Entry struct {
 | 
				
			|||||||
	Location uint64 `json:"location"`
 | 
						Location uint64 `json:"location"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Parse(stdout fmt.Stringer) ([]*Entry, error) {
 | 
					func Parse(p []byte) ([]*Entry, error) {
 | 
				
			||||||
	payload := strings.Split(strings.TrimSpace(stdout.String()), "\n")
 | 
						payload := strings.Split(strings.TrimSpace(string(p)), "\n")
 | 
				
			||||||
	result := make([]*Entry, len(payload))
 | 
						result := make([]*Entry, len(payload))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i, ent := range payload {
 | 
						for i, ent := range payload {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ package ldd_test
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/ldd"
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
@ -34,10 +33,7 @@ libzstd.so.1 => /usr/lib/libzstd.so.1 7ff71bfd2000
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			stdout := new(strings.Builder)
 | 
								if _, err := ldd.Parse([]byte(tc.out)); !errors.Is(err, tc.wantErr) {
 | 
				
			||||||
			stdout.WriteString(tc.out)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if _, err := ldd.Parse(stdout); !errors.Is(err, tc.wantErr) {
 | 
					 | 
				
			||||||
				t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
 | 
									t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@ -111,10 +107,7 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tc.file, func(t *testing.T) {
 | 
							t.Run(tc.file, func(t *testing.T) {
 | 
				
			||||||
			stdout := new(strings.Builder)
 | 
								if got, err := ldd.Parse([]byte(tc.out)); err != nil {
 | 
				
			||||||
			stdout.WriteString(tc.out)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if got, err := ldd.Parse(stdout); err != nil {
 | 
					 | 
				
			||||||
				t.Errorf("Parse() error = %v", err)
 | 
									t.Errorf("Parse() error = %v", err)
 | 
				
			||||||
			} else if !reflect.DeepEqual(got, tc.want) {
 | 
								} else if !reflect.DeepEqual(got, tc.want) {
 | 
				
			||||||
				t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
 | 
									t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								ldd/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ldd/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package ldd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Path returns a deterministic, deduplicated slice of absolute directory paths in entries.
 | 
				
			||||||
 | 
					func Path(entries []*Entry) []string {
 | 
				
			||||||
 | 
						p := make([]string, 0, len(entries)*2)
 | 
				
			||||||
 | 
						for _, entry := range entries {
 | 
				
			||||||
 | 
							if path.IsAbs(entry.Path) {
 | 
				
			||||||
 | 
								p = append(p, path.Dir(entry.Path))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if path.IsAbs(entry.Name) {
 | 
				
			||||||
 | 
								p = append(p, path.Dir(entry.Name))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						slices.Sort(p)
 | 
				
			||||||
 | 
						return slices.Compact(p)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										74
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								main.go
									
									
									
									
									
								
							@ -18,14 +18,12 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
						"git.gensokyo.uk/security/fortify/command"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	init0 "git.gensokyo.uk/security/fortify/internal/app/init"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app/shim"
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/sys"
 | 
						"git.gensokyo.uk/security/fortify/internal/sys"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/system"
 | 
						"git.gensokyo.uk/security/fortify/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,10 +39,10 @@ func init() { fmsg.Prepare("fortify") }
 | 
				
			|||||||
var std sys.State = new(sys.Std)
 | 
					var std sys.State = new(sys.Std)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
 | 
						// early init path, skips root check and duplicate PR_SET_DUMPABLE
 | 
				
			||||||
	init0.TryArgv0()
 | 
						sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
 | 
						if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
		log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
							log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
		// not fatal: this program runs as the privileged user
 | 
							// not fatal: this program runs as the privileged user
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -69,18 +67,13 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
		flagJSON    bool
 | 
							flagJSON    bool
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	c := command.New(out, log.Printf, "fortify", func([]string) error {
 | 
						c := command.New(out, log.Printf, "fortify", func([]string) error {
 | 
				
			||||||
		fmsg.Store(flagVerbose)
 | 
							internal.InstallFmsg(flagVerbose)
 | 
				
			||||||
		if flagVerbose {
 | 
					 | 
				
			||||||
			seccomp.CPrintln = log.Println
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}).
 | 
						}).
 | 
				
			||||||
		Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
							Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
 | 
				
			||||||
		Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
							Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// internal commands
 | 
						c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); 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 {
 | 
				
			||||||
@ -89,10 +82,9 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// config extraArgs...
 | 
							// config extraArgs...
 | 
				
			||||||
		config := tryPath(args[0])
 | 
							config := tryPath(args[0])
 | 
				
			||||||
		config.Command = append(config.Command, args[1:]...)
 | 
							config.Args = append(config.Args, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// invoke app
 | 
							runApp(config)
 | 
				
			||||||
		runApp(app.MustNew(std), config)
 | 
					 | 
				
			||||||
		panic("unreachable")
 | 
							panic("unreachable")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -103,19 +95,20 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			mpris             bool
 | 
								mpris             bool
 | 
				
			||||||
			dbusVerbose       bool
 | 
								dbusVerbose       bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			fid         string
 | 
								fid      string
 | 
				
			||||||
			aid         int
 | 
								aid      int
 | 
				
			||||||
			groups      command.RepeatableFlag
 | 
								groups   command.RepeatableFlag
 | 
				
			||||||
			homeDir     string
 | 
								homeDir  string
 | 
				
			||||||
			userName    string
 | 
								userName string
 | 
				
			||||||
			enablements [system.ELen]bool
 | 
					
 | 
				
			||||||
 | 
								wayland, x11, dBus, pulse bool
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
							c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
 | 
				
			||||||
			// initialise config from flags
 | 
								// initialise config from flags
 | 
				
			||||||
			config := &fst.Config{
 | 
								config := &fst.Config{
 | 
				
			||||||
				ID:      fid,
 | 
									ID:   fid,
 | 
				
			||||||
				Command: args,
 | 
									Args: args,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if aid < 0 || aid > 9999 {
 | 
								if aid < 0 || aid > 9999 {
 | 
				
			||||||
@ -165,15 +158,21 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			config.Confinement.Outer = homeDir
 | 
								config.Confinement.Outer = homeDir
 | 
				
			||||||
			config.Confinement.Username = userName
 | 
								config.Confinement.Username = userName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// enablements from flags
 | 
								if wayland {
 | 
				
			||||||
			for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
 | 
									config.Confinement.Enablements |= system.EWayland
 | 
				
			||||||
				if enablements[i] {
 | 
								}
 | 
				
			||||||
					config.Confinement.Enablements.Set(i)
 | 
								if x11 {
 | 
				
			||||||
				}
 | 
									config.Confinement.Enablements |= system.EX11
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if dBus {
 | 
				
			||||||
 | 
									config.Confinement.Enablements |= system.EDBus
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if pulse {
 | 
				
			||||||
 | 
									config.Confinement.Enablements |= system.EPulse
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// parse D-Bus config file from flags if applicable
 | 
								// parse D-Bus config file from flags if applicable
 | 
				
			||||||
			if enablements[system.EDBus] {
 | 
								if dBus {
 | 
				
			||||||
				if dbusConfigSession == "builtin" {
 | 
									if dbusConfigSession == "builtin" {
 | 
				
			||||||
					config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
 | 
										config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
@ -201,7 +200,7 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// invoke app
 | 
								// invoke app
 | 
				
			||||||
			runApp(app.MustNew(std), config)
 | 
								runApp(config)
 | 
				
			||||||
			panic("unreachable")
 | 
								panic("unreachable")
 | 
				
			||||||
		}).
 | 
							}).
 | 
				
			||||||
			Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
								Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
 | 
				
			||||||
@ -222,13 +221,13 @@ func buildCommand(out io.Writer) command.Command {
 | 
				
			|||||||
				"Application home directory").
 | 
									"Application home directory").
 | 
				
			||||||
			Flag(&userName, "u", command.StringFlag("chronos"),
 | 
								Flag(&userName, "u", command.StringFlag("chronos"),
 | 
				
			||||||
				"Passwd name within sandbox").
 | 
									"Passwd name within sandbox").
 | 
				
			||||||
			Flag(&enablements[system.EWayland], "wayland", command.BoolFlag(false),
 | 
								Flag(&wayland, "wayland", command.BoolFlag(false),
 | 
				
			||||||
				"Allow Wayland connections").
 | 
									"Allow Wayland connections").
 | 
				
			||||||
			Flag(&enablements[system.EX11], "X", command.BoolFlag(false),
 | 
								Flag(&x11, "X", command.BoolFlag(false),
 | 
				
			||||||
				"Share X11 socket and allow connection").
 | 
									"Share X11 socket and allow connection").
 | 
				
			||||||
			Flag(&enablements[system.EDBus], "dbus", command.BoolFlag(false),
 | 
								Flag(&dBus, "dbus", command.BoolFlag(false),
 | 
				
			||||||
				"Proxy D-Bus connection").
 | 
									"Proxy D-Bus connection").
 | 
				
			||||||
			Flag(&enablements[system.EPulse], "pulse", command.BoolFlag(false),
 | 
								Flag(&pulse, "pulse", command.BoolFlag(false),
 | 
				
			||||||
				"Share PulseAudio socket and cookie")
 | 
									"Share PulseAudio socket and cookie")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -281,10 +280,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 {
 | 
				
			||||||
@ -292,7 +292,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)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										55
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								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 = {
 | 
				
			||||||
@ -83,14 +84,16 @@ in
 | 
				
			|||||||
                  command = if app.command == null then app.name else app.command;
 | 
					                  command = if app.command == null then app.name else app.command;
 | 
				
			||||||
                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
					                  script = if app.script == null then ("exec " + command + " $@") else app.script;
 | 
				
			||||||
                  enablements = 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);
 | 
				
			||||||
 | 
					                  isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  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,18 +101,17 @@ in
 | 
				
			|||||||
                      home = getsubhome fid aid;
 | 
					                      home = getsubhome fid aid;
 | 
				
			||||||
                      sandbox = {
 | 
					                      sandbox = {
 | 
				
			||||||
                        inherit (app)
 | 
					                        inherit (app)
 | 
				
			||||||
 | 
					                          devel
 | 
				
			||||||
                          userns
 | 
					                          userns
 | 
				
			||||||
                          net
 | 
					                          net
 | 
				
			||||||
                          dev
 | 
					                          dev
 | 
				
			||||||
 | 
					                          tty
 | 
				
			||||||
 | 
					                          multiarch
 | 
				
			||||||
                          env
 | 
					                          env
 | 
				
			||||||
                          ;
 | 
					                          ;
 | 
				
			||||||
                        syscall = {
 | 
					 | 
				
			||||||
                          inherit (app) compat multiarch bluetooth;
 | 
					 | 
				
			||||||
                          deny_devel = !app.devel;
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                        map_real_uid = app.mapRealUid;
 | 
					                        map_real_uid = app.mapRealUid;
 | 
				
			||||||
                        no_new_session = app.tty;
 | 
					 | 
				
			||||||
                        direct_wayland = app.insecureWayland;
 | 
					                        direct_wayland = app.insecureWayland;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        filesystem =
 | 
					                        filesystem =
 | 
				
			||||||
                          let
 | 
					                          let
 | 
				
			||||||
                            bind = src: { inherit src; };
 | 
					                            bind = src: { inherit src; };
 | 
				
			||||||
@ -126,7 +128,6 @@ in
 | 
				
			|||||||
                            (mustBind "/bin")
 | 
					                            (mustBind "/bin")
 | 
				
			||||||
                            (mustBind "/usr/bin")
 | 
					                            (mustBind "/usr/bin")
 | 
				
			||||||
                            (mustBind "/nix/store")
 | 
					                            (mustBind "/nix/store")
 | 
				
			||||||
                            (mustBind "/run/current-system")
 | 
					 | 
				
			||||||
                            (bind "/sys/block")
 | 
					                            (bind "/sys/block")
 | 
				
			||||||
                            (bind "/sys/bus")
 | 
					                            (bind "/sys/bus")
 | 
				
			||||||
                            (bind "/sys/class")
 | 
					                            (bind "/sys/class")
 | 
				
			||||||
@ -137,8 +138,7 @@ in
 | 
				
			|||||||
                            (mustBind "/nix/var")
 | 
					                            (mustBind "/nix/var")
 | 
				
			||||||
                            (bind "/var/db/nix-channels")
 | 
					                            (bind "/var/db/nix-channels")
 | 
				
			||||||
                          ]
 | 
					                          ]
 | 
				
			||||||
                          ++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
 | 
					                          ++ optionals isGraphical [
 | 
				
			||||||
                            (bind "/run/opengl-driver")
 | 
					 | 
				
			||||||
                            (devBind "/dev/dri")
 | 
					                            (devBind "/dev/dri")
 | 
				
			||||||
                            (devBind "/dev/nvidiactl")
 | 
					                            (devBind "/dev/nvidiactl")
 | 
				
			||||||
                            (devBind "/dev/nvidia-modeset")
 | 
					                            (devBind "/dev/nvidia-modeset")
 | 
				
			||||||
@ -148,8 +148,31 @@ in
 | 
				
			|||||||
                          ]
 | 
					                          ]
 | 
				
			||||||
                          ++ app.extraPaths;
 | 
					                          ++ app.extraPaths;
 | 
				
			||||||
                        auto_etc = true;
 | 
					                        auto_etc = true;
 | 
				
			||||||
                        override = [ "/var/run/nscd" ];
 | 
					                        cover = [ "/var/run/nscd" ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        symlink =
 | 
				
			||||||
 | 
					                          [
 | 
				
			||||||
 | 
					                            [
 | 
				
			||||||
 | 
					                              "*/run/current-system"
 | 
				
			||||||
 | 
					                              "/run/current-system"
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                          ]
 | 
				
			||||||
 | 
					                          ++ optionals (isGraphical && config.hardware.graphics.enable) (
 | 
				
			||||||
 | 
					                            [
 | 
				
			||||||
 | 
					                              [
 | 
				
			||||||
 | 
					                                config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
 | 
				
			||||||
 | 
					                                "/run/opengl-driver"
 | 
				
			||||||
 | 
					                              ]
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                            ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
 | 
				
			||||||
 | 
					                              [
 | 
				
			||||||
 | 
					                                config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
 | 
				
			||||||
 | 
					                                /run/opengl-driver-32
 | 
				
			||||||
 | 
					                              ]
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
                      };
 | 
					                      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      inherit enablements;
 | 
					                      inherit enablements;
 | 
				
			||||||
                      inherit (dbusConfig) session_bus system_bus;
 | 
					                      inherit (dbusConfig) session_bus system_bus;
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										94
									
								
								options.md
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								options.md
									
									
									
									
									
								
							@ -21,7 +21,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.package
 | 
					## environment\.fortify\.package
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,8 +35,7 @@ package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*Default:*
 | 
					*Default:*
 | 
				
			||||||
` <derivation fortify-static-x86_64-unknown-linux-musl-0.2.18> `
 | 
					` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.1> `
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,7 +55,6 @@ list of (submodule)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.packages
 | 
					## environment\.fortify\.apps\.\*\.packages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,31 +73,6 @@ list of package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.bluetooth
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Whether to enable AF_BLUETOOTH socket operations\.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Type:*
 | 
					 | 
				
			||||||
boolean
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Default:*
 | 
					 | 
				
			||||||
` false `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Example:*
 | 
					 | 
				
			||||||
` true `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.capability\.dbus
 | 
					## environment\.fortify\.apps\.\*\.capability\.dbus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -119,7 +91,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.capability\.pulse
 | 
					## environment\.fortify\.apps\.\*\.capability\.pulse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -138,7 +109,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.capability\.wayland
 | 
					## environment\.fortify\.apps\.\*\.capability\.wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -157,7 +127,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.capability\.x11
 | 
					## environment\.fortify\.apps\.\*\.capability\.x11
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -176,7 +145,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.command
 | 
					## environment\.fortify\.apps\.\*\.command
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,31 +165,6 @@ null or string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.compat
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Whether to enable disable syscall filter extensions\.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Type:*
 | 
					 | 
				
			||||||
boolean
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Default:*
 | 
					 | 
				
			||||||
` false `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Example:*
 | 
					 | 
				
			||||||
` true `
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.dbus\.session
 | 
					## environment\.fortify\.apps\.\*\.dbus\.session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -241,7 +184,6 @@ null or (function that evaluates to a(n) anything)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.dbus\.system
 | 
					## environment\.fortify\.apps\.\*\.dbus\.system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -261,7 +203,6 @@ null or anything
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.dev
 | 
					## environment\.fortify\.apps\.\*\.dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -285,12 +226,11 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.devel
 | 
					## environment\.fortify\.apps\.\*\.devel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable development kernel APIs\.
 | 
					Whether to enable debugging-related kernel interfaces\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -309,7 +249,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.env
 | 
					## environment\.fortify\.apps\.\*\.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -328,7 +267,6 @@ null or (attribute set of string)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.extraConfig
 | 
					## environment\.fortify\.apps\.\*\.extraConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -347,7 +285,6 @@ anything
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.extraPaths
 | 
					## environment\.fortify\.apps\.\*\.extraPaths
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -366,7 +303,6 @@ list of anything
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.gpu
 | 
					## environment\.fortify\.apps\.\*\.gpu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -386,7 +322,6 @@ null or boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.groups
 | 
					## environment\.fortify\.apps\.\*\.groups
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -405,7 +340,6 @@ list of string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.id
 | 
					## environment\.fortify\.apps\.\*\.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -424,7 +358,6 @@ null or string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.insecureWayland
 | 
					## environment\.fortify\.apps\.\*\.insecureWayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -448,7 +381,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.mapRealUid
 | 
					## environment\.fortify\.apps\.\*\.mapRealUid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -472,12 +404,11 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.multiarch
 | 
					## environment\.fortify\.apps\.\*\.multiarch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable multiarch kernel support\.
 | 
					Whether to enable multiarch kernel-level support\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -496,7 +427,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.name
 | 
					## environment\.fortify\.apps\.\*\.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -510,7 +440,6 @@ string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.net
 | 
					## environment\.fortify\.apps\.\*\.net
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -534,12 +463,11 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.nix
 | 
					## environment\.fortify\.apps\.\*\.nix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable nix daemon\.
 | 
					Whether to enable nix daemon access\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -558,7 +486,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.script
 | 
					## environment\.fortify\.apps\.\*\.script
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -577,7 +504,6 @@ null or string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.share
 | 
					## environment\.fortify\.apps\.\*\.share
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -597,7 +523,6 @@ null or package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.tty
 | 
					## environment\.fortify\.apps\.\*\.tty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -621,12 +546,11 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.userns
 | 
					## environment\.fortify\.apps\.\*\.userns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable user namespace\.
 | 
					Whether to enable user namespace creation\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -645,7 +569,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.apps\.\*\.verbose
 | 
					## environment\.fortify\.apps\.\*\.verbose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -669,7 +592,6 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.fsuPackage
 | 
					## environment\.fortify\.fsuPackage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -684,8 +606,7 @@ package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*Default:*
 | 
					*Default:*
 | 
				
			||||||
` <derivation fortify-fsu-0.2.18> `
 | 
					` <derivation fortify-fsu-0.3.1> `
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -702,7 +623,6 @@ function that evaluates to a(n) function that evaluates to a(n) attribute set of
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.stateDir
 | 
					## environment\.fortify\.stateDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -716,7 +636,6 @@ string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## environment\.fortify\.users
 | 
					## environment\.fortify\.users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -729,4 +648,3 @@ Users allowed to spawn fortify apps and their corresponding fortify fid\.
 | 
				
			|||||||
attribute set of integer between 0 and 99 (both inclusive)
 | 
					attribute set of integer between 0 and 99 (both inclusive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								package.nix
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.nix
									
									
									
									
									
								
							@ -4,7 +4,6 @@
 | 
				
			|||||||
  buildGoModule,
 | 
					  buildGoModule,
 | 
				
			||||||
  makeBinaryWrapper,
 | 
					  makeBinaryWrapper,
 | 
				
			||||||
  xdg-dbus-proxy,
 | 
					  xdg-dbus-proxy,
 | 
				
			||||||
  bubblewrap,
 | 
					 | 
				
			||||||
  pkg-config,
 | 
					  pkg-config,
 | 
				
			||||||
  libffi,
 | 
					  libffi,
 | 
				
			||||||
  libseccomp,
 | 
					  libseccomp,
 | 
				
			||||||
@ -19,18 +18,25 @@
 | 
				
			|||||||
  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,
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
buildGoModule rec {
 | 
					buildGoModule rec {
 | 
				
			||||||
  pname = "fortify";
 | 
					  pname = "fortify";
 | 
				
			||||||
  version = "0.2.18";
 | 
					  version = "0.3.1";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -83,7 +89,6 @@ buildGoModule rec {
 | 
				
			|||||||
    let
 | 
					    let
 | 
				
			||||||
      appPackages = [
 | 
					      appPackages = [
 | 
				
			||||||
        glibc
 | 
					        glibc
 | 
				
			||||||
        bubblewrap
 | 
					 | 
				
			||||||
        xdg-dbus-proxy
 | 
					        xdg-dbus-proxy
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
    in
 | 
					    in
 | 
				
			||||||
@ -108,4 +113,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",
 | 
				
			||||||
 | 
				
			|||||||
@ -37,13 +37,13 @@ func Test_printShowInstance(t *testing.T) {
 | 
				
			|||||||
	}{
 | 
						}{
 | 
				
			||||||
		{"config", nil, fst.Template(), false, false, `App
 | 
							{"config", nil, fst.Template(), false, false, `App
 | 
				
			||||||
 ID:             9 (org.chromium.Chromium)
 | 
					 ID:             9 (org.chromium.Chromium)
 | 
				
			||||||
 Enablements:    Wayland, D-Bus, PulseAudio
 | 
					 Enablements:    wayland, dbus, pulseaudio
 | 
				
			||||||
 Groups:         ["video"]
 | 
					 Groups:         ["video"]
 | 
				
			||||||
 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
					 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
				
			||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -74,14 +74,14 @@ System bus
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
		{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
 | 
							{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Flags:          none
 | 
					 Flags:          none
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
@ -90,7 +90,7 @@ App
 | 
				
			|||||||
`},
 | 
					`},
 | 
				
			||||||
		{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
 | 
							{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Flags:          none
 | 
					 Flags:          none
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
@ -105,7 +105,7 @@ Extra ACL
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -121,13 +121,13 @@ Session bus
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             9 (org.chromium.Chromium)
 | 
					 ID:             9 (org.chromium.Chromium)
 | 
				
			||||||
 Enablements:    Wayland, D-Bus, PulseAudio
 | 
					 Enablements:    wayland, dbus, pulseaudio
 | 
				
			||||||
 Groups:         ["video"]
 | 
					 Groups:         ["video"]
 | 
				
			||||||
 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
					 Directory:      /var/lib/persist/home/org.chromium.Chromium
 | 
				
			||||||
 Hostname:       "localhost"
 | 
					 Hostname:       "localhost"
 | 
				
			||||||
 Flags:          userns net dev tty mapuid autoetc
 | 
					 Flags:          userns net dev tty mapuid autoetc
 | 
				
			||||||
 Etc:            /etc
 | 
					 Etc:            /etc
 | 
				
			||||||
 Overrides:      /var/run/nscd
 | 
					 Cover:          /var/run/nscd
 | 
				
			||||||
 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
					 Command:        chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filesystem
 | 
					Filesystem
 | 
				
			||||||
@ -162,7 +162,7 @@ State
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
App
 | 
					App
 | 
				
			||||||
 ID:             0
 | 
					 ID:             0
 | 
				
			||||||
 Enablements:    (No enablements)
 | 
					 Enablements:    (no enablements)
 | 
				
			||||||
 Directory:      
 | 
					 Directory:      
 | 
				
			||||||
 Command:        
 | 
					 Command:        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -192,7 +192,8 @@ App
 | 
				
			|||||||
  "pid": 3735928559,
 | 
					  "pid": 3735928559,
 | 
				
			||||||
  "config": {
 | 
					  "config": {
 | 
				
			||||||
    "id": "org.chromium.Chromium",
 | 
					    "id": "org.chromium.Chromium",
 | 
				
			||||||
    "command": [
 | 
					    "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					    "args": [
 | 
				
			||||||
      "chromium",
 | 
					      "chromium",
 | 
				
			||||||
      "--ignore-gpu-blocklist",
 | 
					      "--ignore-gpu-blocklist",
 | 
				
			||||||
      "--disable-smooth-scrolling",
 | 
					      "--disable-smooth-scrolling",
 | 
				
			||||||
@ -209,24 +210,19 @@ App
 | 
				
			|||||||
      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					      "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
      "sandbox": {
 | 
					      "sandbox": {
 | 
				
			||||||
        "hostname": "localhost",
 | 
					        "hostname": "localhost",
 | 
				
			||||||
 | 
					        "seccomp": 32,
 | 
				
			||||||
 | 
					        "devel": true,
 | 
				
			||||||
        "userns": true,
 | 
					        "userns": true,
 | 
				
			||||||
        "net": true,
 | 
					        "net": true,
 | 
				
			||||||
        "dev": true,
 | 
					        "tty": true,
 | 
				
			||||||
        "syscall": {
 | 
					        "multiarch": true,
 | 
				
			||||||
          "compat": false,
 | 
					 | 
				
			||||||
          "deny_devel": true,
 | 
					 | 
				
			||||||
          "multiarch": true,
 | 
					 | 
				
			||||||
          "linux32": false,
 | 
					 | 
				
			||||||
          "can": false,
 | 
					 | 
				
			||||||
          "bluetooth": false
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "no_new_session": true,
 | 
					 | 
				
			||||||
        "map_real_uid": true,
 | 
					 | 
				
			||||||
        "env": {
 | 
					        "env": {
 | 
				
			||||||
          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					          "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					          "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					          "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "map_real_uid": true,
 | 
				
			||||||
 | 
					        "dev": true,
 | 
				
			||||||
        "filesystem": [
 | 
					        "filesystem": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "src": "/nix/store"
 | 
					            "src": "/nix/store"
 | 
				
			||||||
@ -259,7 +255,7 @@ App
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "etc": "/etc",
 | 
					        "etc": "/etc",
 | 
				
			||||||
        "auto_etc": true,
 | 
					        "auto_etc": true,
 | 
				
			||||||
        "override": [
 | 
					        "cover": [
 | 
				
			||||||
          "/var/run/nscd"
 | 
					          "/var/run/nscd"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -320,7 +316,8 @@ App
 | 
				
			|||||||
`},
 | 
					`},
 | 
				
			||||||
		{"json config", nil, fst.Template(), false, true, `{
 | 
							{"json config", nil, fst.Template(), false, true, `{
 | 
				
			||||||
  "id": "org.chromium.Chromium",
 | 
					  "id": "org.chromium.Chromium",
 | 
				
			||||||
  "command": [
 | 
					  "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					  "args": [
 | 
				
			||||||
    "chromium",
 | 
					    "chromium",
 | 
				
			||||||
    "--ignore-gpu-blocklist",
 | 
					    "--ignore-gpu-blocklist",
 | 
				
			||||||
    "--disable-smooth-scrolling",
 | 
					    "--disable-smooth-scrolling",
 | 
				
			||||||
@ -337,24 +334,19 @@ App
 | 
				
			|||||||
    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					    "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
    "sandbox": {
 | 
					    "sandbox": {
 | 
				
			||||||
      "hostname": "localhost",
 | 
					      "hostname": "localhost",
 | 
				
			||||||
 | 
					      "seccomp": 32,
 | 
				
			||||||
 | 
					      "devel": true,
 | 
				
			||||||
      "userns": true,
 | 
					      "userns": true,
 | 
				
			||||||
      "net": true,
 | 
					      "net": true,
 | 
				
			||||||
      "dev": true,
 | 
					      "tty": true,
 | 
				
			||||||
      "syscall": {
 | 
					      "multiarch": true,
 | 
				
			||||||
        "compat": false,
 | 
					 | 
				
			||||||
        "deny_devel": true,
 | 
					 | 
				
			||||||
        "multiarch": true,
 | 
					 | 
				
			||||||
        "linux32": false,
 | 
					 | 
				
			||||||
        "can": false,
 | 
					 | 
				
			||||||
        "bluetooth": false
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "no_new_session": true,
 | 
					 | 
				
			||||||
      "map_real_uid": true,
 | 
					 | 
				
			||||||
      "env": {
 | 
					      "env": {
 | 
				
			||||||
        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					        "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					        "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					        "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "map_real_uid": true,
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
      "filesystem": [
 | 
					      "filesystem": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "src": "/nix/store"
 | 
					          "src": "/nix/store"
 | 
				
			||||||
@ -387,7 +379,7 @@ App
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      "etc": "/etc",
 | 
					      "etc": "/etc",
 | 
				
			||||||
      "auto_etc": true,
 | 
					      "auto_etc": true,
 | 
				
			||||||
      "override": [
 | 
					      "cover": [
 | 
				
			||||||
        "/var/run/nscd"
 | 
					        "/var/run/nscd"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -477,8 +469,8 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		{"valid", state.Entries{testID: testState}, false, false, `    Instance    PID           App    Uptime     Enablements                   Command
 | 
							{"valid", state.Entries{testID: testState}, false, false, `    Instance    PID           App    Uptime     Enablements                  Command
 | 
				
			||||||
    8e2c76b0    3735928559    9      1h2m32s    Wayland, D-Bus, PulseAudio    ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
 | 
					    8e2c76b0    3735928559    9      1h2m32s    wayland, dbus, pulseaudio    ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`},
 | 
					`},
 | 
				
			||||||
		{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
 | 
							{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
 | 
				
			||||||
@ -506,7 +498,8 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
    "pid": 3735928559,
 | 
					    "pid": 3735928559,
 | 
				
			||||||
    "config": {
 | 
					    "config": {
 | 
				
			||||||
      "id": "org.chromium.Chromium",
 | 
					      "id": "org.chromium.Chromium",
 | 
				
			||||||
      "command": [
 | 
					      "path": "/run/current-system/sw/bin/chromium",
 | 
				
			||||||
 | 
					      "args": [
 | 
				
			||||||
        "chromium",
 | 
					        "chromium",
 | 
				
			||||||
        "--ignore-gpu-blocklist",
 | 
					        "--ignore-gpu-blocklist",
 | 
				
			||||||
        "--disable-smooth-scrolling",
 | 
					        "--disable-smooth-scrolling",
 | 
				
			||||||
@ -523,24 +516,19 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
					        "home": "/var/lib/persist/home/org.chromium.Chromium",
 | 
				
			||||||
        "sandbox": {
 | 
					        "sandbox": {
 | 
				
			||||||
          "hostname": "localhost",
 | 
					          "hostname": "localhost",
 | 
				
			||||||
 | 
					          "seccomp": 32,
 | 
				
			||||||
 | 
					          "devel": true,
 | 
				
			||||||
          "userns": true,
 | 
					          "userns": true,
 | 
				
			||||||
          "net": true,
 | 
					          "net": true,
 | 
				
			||||||
          "dev": true,
 | 
					          "tty": true,
 | 
				
			||||||
          "syscall": {
 | 
					          "multiarch": true,
 | 
				
			||||||
            "compat": false,
 | 
					 | 
				
			||||||
            "deny_devel": true,
 | 
					 | 
				
			||||||
            "multiarch": true,
 | 
					 | 
				
			||||||
            "linux32": false,
 | 
					 | 
				
			||||||
            "can": false,
 | 
					 | 
				
			||||||
            "bluetooth": false
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "no_new_session": true,
 | 
					 | 
				
			||||||
          "map_real_uid": true,
 | 
					 | 
				
			||||||
          "env": {
 | 
					          "env": {
 | 
				
			||||||
            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
					            "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
					            "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
 | 
				
			||||||
            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
					            "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "map_real_uid": true,
 | 
				
			||||||
 | 
					          "dev": true,
 | 
				
			||||||
          "filesystem": [
 | 
					          "filesystem": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "src": "/nix/store"
 | 
					              "src": "/nix/store"
 | 
				
			||||||
@ -573,7 +561,7 @@ func Test_printPs(t *testing.T) {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
          "etc": "/etc",
 | 
					          "etc": "/etc",
 | 
				
			||||||
          "auto_etc": true,
 | 
					          "auto_etc": true,
 | 
				
			||||||
          "override": [
 | 
					          "cover": [
 | 
				
			||||||
            "/var/run/nscd"
 | 
					            "/var/run/nscd"
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										252
									
								
								sandbox/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								sandbox/container.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,252 @@
 | 
				
			|||||||
 | 
					// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
 | 
				
			||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HardeningFlags uintptr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						FSyscallCompat HardeningFlags = 1 << iota
 | 
				
			||||||
 | 
						FAllowDevel
 | 
				
			||||||
 | 
						FAllowUserns
 | 
				
			||||||
 | 
						FAllowTTY
 | 
				
			||||||
 | 
						FAllowNet
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
 | 
				
			||||||
 | 
						if flags&FSyscallCompat == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagExt
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowDevel == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyDevel
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowUserns == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyNS
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if flags&FAllowTTY == 0 {
 | 
				
			||||||
 | 
							opts |= seccomp.FlagDenyTTY
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return opts
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type (
 | 
				
			||||||
 | 
						// Container represents a container environment being prepared or run.
 | 
				
			||||||
 | 
						// None of [Container] methods are safe for concurrent use.
 | 
				
			||||||
 | 
						Container struct {
 | 
				
			||||||
 | 
							// Name of initial process in the container.
 | 
				
			||||||
 | 
							name string
 | 
				
			||||||
 | 
							// Cgroup fd, nil to disable.
 | 
				
			||||||
 | 
							Cgroup *int
 | 
				
			||||||
 | 
							// ExtraFiles passed through to initial process in the container,
 | 
				
			||||||
 | 
							// with behaviour identical to its [exec.Cmd] counterpart.
 | 
				
			||||||
 | 
							ExtraFiles []*os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Custom [exec.Cmd] initialisation function.
 | 
				
			||||||
 | 
							CommandContext func(ctx context.Context) (cmd *exec.Cmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// param encoder for shim and init
 | 
				
			||||||
 | 
							setup *gob.Encoder
 | 
				
			||||||
 | 
							// cancels cmd
 | 
				
			||||||
 | 
							cancel context.CancelFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Stdin  io.Reader
 | 
				
			||||||
 | 
							Stdout io.Writer
 | 
				
			||||||
 | 
							Stderr io.Writer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Cancel    func(cmd *exec.Cmd) error
 | 
				
			||||||
 | 
							WaitDelay time.Duration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cmd *exec.Cmd
 | 
				
			||||||
 | 
							ctx context.Context
 | 
				
			||||||
 | 
							Params
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Params holds container configuration and is safe to serialise.
 | 
				
			||||||
 | 
						Params struct {
 | 
				
			||||||
 | 
							// Working directory in the container.
 | 
				
			||||||
 | 
							Dir string
 | 
				
			||||||
 | 
							// Initial process environment.
 | 
				
			||||||
 | 
							Env []string
 | 
				
			||||||
 | 
							// Absolute path of initial process in the container. Overrides name.
 | 
				
			||||||
 | 
							Path string
 | 
				
			||||||
 | 
							// Initial process argv.
 | 
				
			||||||
 | 
							Args []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Mapped Uid in user namespace.
 | 
				
			||||||
 | 
							Uid int
 | 
				
			||||||
 | 
							// Mapped Gid in user namespace.
 | 
				
			||||||
 | 
							Gid int
 | 
				
			||||||
 | 
							// Hostname value in UTS namespace.
 | 
				
			||||||
 | 
							Hostname string
 | 
				
			||||||
 | 
							// Sequential container setup ops.
 | 
				
			||||||
 | 
							*Ops
 | 
				
			||||||
 | 
							// Extra seccomp options.
 | 
				
			||||||
 | 
							Seccomp seccomp.SyscallOpts
 | 
				
			||||||
 | 
							// Permission bits of newly created parent directories.
 | 
				
			||||||
 | 
							// The zero value is interpreted as 0755.
 | 
				
			||||||
 | 
							ParentPerm os.FileMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Flags HardeningFlags
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Ops []Op
 | 
				
			||||||
 | 
						Op  interface {
 | 
				
			||||||
 | 
							early(params *Params) error
 | 
				
			||||||
 | 
							apply(params *Params) error
 | 
				
			||||||
 | 
							prefix() string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Is(op Op) bool
 | 
				
			||||||
 | 
							fmt.Stringer
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Start() error {
 | 
				
			||||||
 | 
						if p.cmd != nil {
 | 
				
			||||||
 | 
							return errors.New("sandbox: already started")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.Ops == nil || len(*p.Ops) == 0 {
 | 
				
			||||||
 | 
							return errors.New("sandbox: starting an empty container")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(p.ctx)
 | 
				
			||||||
 | 
						p.cancel = cancel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var cloneFlags uintptr = syscall.CLONE_NEWIPC |
 | 
				
			||||||
 | 
							syscall.CLONE_NEWUTS |
 | 
				
			||||||
 | 
							syscall.CLONE_NEWCGROUP
 | 
				
			||||||
 | 
						if p.Flags&FAllowNet == 0 {
 | 
				
			||||||
 | 
							cloneFlags |= syscall.CLONE_NEWNET
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// map to overflow id to work around ownership checks
 | 
				
			||||||
 | 
						if p.Uid < 1 {
 | 
				
			||||||
 | 
							p.Uid = OverflowUid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.Gid < 1 {
 | 
				
			||||||
 | 
							p.Gid = OverflowGid()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.CommandContext != nil {
 | 
				
			||||||
 | 
							p.cmd = p.CommandContext(ctx)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.cmd = exec.CommandContext(ctx, MustExecutable())
 | 
				
			||||||
 | 
							p.cmd.Args = []string{"init"}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
 | 
				
			||||||
 | 
						p.cmd.WaitDelay = p.WaitDelay
 | 
				
			||||||
 | 
						if p.Cancel != nil {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cmd.Dir = "/"
 | 
				
			||||||
 | 
						p.cmd.SysProcAttr = &syscall.SysProcAttr{
 | 
				
			||||||
 | 
							Setsid:    p.Flags&FAllowTTY == 0,
 | 
				
			||||||
 | 
							Pdeathsig: syscall.SIGKILL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Cloneflags: cloneFlags |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWUSER |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWPID |
 | 
				
			||||||
 | 
								syscall.CLONE_NEWNS,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// remain privileged for setup
 | 
				
			||||||
 | 
							AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							UseCgroupFD: p.Cgroup != nil,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if p.cmd.SysProcAttr.UseCgroupFD {
 | 
				
			||||||
 | 
							p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// place setup pipe before user supplied extra files, this is later restored by init
 | 
				
			||||||
 | 
						if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
 | 
								"cannot create shim setup pipe:")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p.setup = e
 | 
				
			||||||
 | 
							p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg.Verbose("starting container init")
 | 
				
			||||||
 | 
						if err := p.cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Serve() error {
 | 
				
			||||||
 | 
						if p.setup == nil {
 | 
				
			||||||
 | 
							panic("invalid serve")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup := p.setup
 | 
				
			||||||
 | 
						p.setup = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.Path != "" && !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EINVAL,
 | 
				
			||||||
 | 
								fmt.Sprintf("invalid executable path %q", p.Path))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.Path == "" {
 | 
				
			||||||
 | 
							if p.name == "" {
 | 
				
			||||||
 | 
								p.Path = os.Getenv("SHELL")
 | 
				
			||||||
 | 
								if !path.IsAbs(p.Path) {
 | 
				
			||||||
 | 
									p.cancel()
 | 
				
			||||||
 | 
									return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
										"no command specified and $SHELL is invalid")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.name = path.Base(p.Path)
 | 
				
			||||||
 | 
							} else if path.IsAbs(p.name) {
 | 
				
			||||||
 | 
								p.Path = p.name
 | 
				
			||||||
 | 
							} else if v, err := exec.LookPath(p.name); err != nil {
 | 
				
			||||||
 | 
								p.cancel()
 | 
				
			||||||
 | 
								return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								p.Path = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := setup.Encode(
 | 
				
			||||||
 | 
							&initParams{
 | 
				
			||||||
 | 
								p.Params,
 | 
				
			||||||
 | 
								syscall.Getuid(),
 | 
				
			||||||
 | 
								syscall.Getgid(),
 | 
				
			||||||
 | 
								len(p.ExtraFiles),
 | 
				
			||||||
 | 
								msg.IsVerbose(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							p.cancel()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Container) String() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
 | 
				
			||||||
 | 
							p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(ctx context.Context, name string, args ...string) *Container {
 | 
				
			||||||
 | 
						return &Container{name: name, ctx: ctx,
 | 
				
			||||||
 | 
							Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										255
									
								
								sandbox/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								sandbox/container_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,255 @@
 | 
				
			|||||||
 | 
					package sandbox_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/ldd"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						ignore  = "\x00"
 | 
				
			||||||
 | 
						ignoreV = -1
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainer(t *testing.T) {
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							oldVerbose := fmsg.Load()
 | 
				
			||||||
 | 
							oldOutput := sandbox.GetOutput()
 | 
				
			||||||
 | 
							internal.InstallFmsg(true)
 | 
				
			||||||
 | 
							t.Cleanup(func() { fmsg.Store(oldVerbose) })
 | 
				
			||||||
 | 
							t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name  string
 | 
				
			||||||
 | 
							flags sandbox.HardeningFlags
 | 
				
			||||||
 | 
							ops   *sandbox.Ops
 | 
				
			||||||
 | 
							mnt   []*vfs.MountInfoEntry
 | 
				
			||||||
 | 
							host  string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
 | 
				
			||||||
 | 
							{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
 | 
				
			||||||
 | 
								new(sandbox.Ops), nil, "test-minimal"},
 | 
				
			||||||
 | 
							{"tmpfs", 0,
 | 
				
			||||||
 | 
								new(sandbox.Ops).
 | 
				
			||||||
 | 
									Tmpfs(fst.Tmp, 0, 0755),
 | 
				
			||||||
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
 | 
									e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
 | 
								}, "test-tmpfs"},
 | 
				
			||||||
 | 
							{"dev", sandbox.FAllowTTY, // go test output is not a tty
 | 
				
			||||||
 | 
								new(sandbox.Ops).
 | 
				
			||||||
 | 
									Dev("/dev").
 | 
				
			||||||
 | 
									Mqueue("/dev/mqueue"),
 | 
				
			||||||
 | 
								[]*vfs.MountInfoEntry{
 | 
				
			||||||
 | 
									e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
 | 
				
			||||||
 | 
									e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
 | 
				
			||||||
 | 
									e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
 | 
				
			||||||
 | 
								}, ""},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
								defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
 | 
				
			||||||
 | 
									"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
 | 
				
			||||||
 | 
								container.Uid = 1000
 | 
				
			||||||
 | 
								container.Gid = 100
 | 
				
			||||||
 | 
								container.Hostname = tc.host
 | 
				
			||||||
 | 
								container.CommandContext = commandContext
 | 
				
			||||||
 | 
								container.Flags |= tc.flags
 | 
				
			||||||
 | 
								container.Stdout, container.Stderr = os.Stdout, os.Stderr
 | 
				
			||||||
 | 
								container.Ops = tc.ops
 | 
				
			||||||
 | 
								if container.Args[5] == "" {
 | 
				
			||||||
 | 
									if name, err := os.Hostname(); err != nil {
 | 
				
			||||||
 | 
										t.Fatalf("cannot get hostname: %v", err)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										container.Args[5] = name
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								container.
 | 
				
			||||||
 | 
									Tmpfs("/tmp", 0, 0755).
 | 
				
			||||||
 | 
									Bind(os.Args[0], os.Args[0], 0).
 | 
				
			||||||
 | 
									Mkdir("/usr/bin", 0755).
 | 
				
			||||||
 | 
									Link(os.Args[0], "/usr/bin/sandbox.test").
 | 
				
			||||||
 | 
									Place("/etc/hostname", []byte(container.Args[5]))
 | 
				
			||||||
 | 
								// in case test has cgo enabled
 | 
				
			||||||
 | 
								var libPaths []string
 | 
				
			||||||
 | 
								if entries, err := ldd.ExecFilter(ctx,
 | 
				
			||||||
 | 
									commandContext,
 | 
				
			||||||
 | 
									func(v []byte) []byte {
 | 
				
			||||||
 | 
										return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
 | 
				
			||||||
 | 
									}, os.Args[0]); err != nil {
 | 
				
			||||||
 | 
									log.Fatalf("ldd: %v", err)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									libPaths = ldd.Path(entries)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, name := range libPaths {
 | 
				
			||||||
 | 
									container.Bind(name, name, 0)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// needs /proc to check mountinfo
 | 
				
			||||||
 | 
								container.Proc("/proc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
 | 
				
			||||||
 | 
								mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
 | 
				
			||||||
 | 
								mnt = append(mnt, tc.mnt...)
 | 
				
			||||||
 | 
								mnt = append(mnt,
 | 
				
			||||||
 | 
									e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
 | 
				
			||||||
 | 
									e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
 | 
				
			||||||
 | 
									e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								for _, name := range libPaths {
 | 
				
			||||||
 | 
									mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
 | 
				
			||||||
 | 
								want := new(bytes.Buffer)
 | 
				
			||||||
 | 
								if err := gob.NewEncoder(want).Encode(mnt); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("cannot serialise expected mount points: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								container.Stdin = want
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := container.Start(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "start:")
 | 
				
			||||||
 | 
									t.Fatalf("cannot start container: %v", err)
 | 
				
			||||||
 | 
								} else if err = container.Serve(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "serve:")
 | 
				
			||||||
 | 
									t.Errorf("cannot serve setup params: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := container.Wait(); err != nil {
 | 
				
			||||||
 | 
									fmsg.PrintBaseError(err, "wait:")
 | 
				
			||||||
 | 
									t.Fatalf("wait: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
 | 
				
			||||||
 | 
						return &vfs.MountInfoEntry{
 | 
				
			||||||
 | 
							ID:        ignoreV,
 | 
				
			||||||
 | 
							Parent:    ignoreV,
 | 
				
			||||||
 | 
							Devno:     vfs.DevT{ignoreV, ignoreV},
 | 
				
			||||||
 | 
							Root:      root,
 | 
				
			||||||
 | 
							Target:    target,
 | 
				
			||||||
 | 
							VfsOptstr: vfsOptstr,
 | 
				
			||||||
 | 
							OptFields: []string{ignore},
 | 
				
			||||||
 | 
							FsType:    fsType,
 | 
				
			||||||
 | 
							Source:    source,
 | 
				
			||||||
 | 
							FsOptstr:  fsOptstr,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestContainerString(t *testing.T) {
 | 
				
			||||||
 | 
						container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
 | 
				
			||||||
 | 
						container.Flags |= sandbox.FAllowDevel
 | 
				
			||||||
 | 
						container.Seccomp |= seccomp.FlagMultiarch
 | 
				
			||||||
 | 
						want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
 | 
				
			||||||
 | 
						if got := container.String(); got != want {
 | 
				
			||||||
 | 
							t.Errorf("String: %s, want %s", got, want)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperInit(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 5 || os.Args[4] != "init" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sandbox.SetOutput(fmsg.Output{})
 | 
				
			||||||
 | 
						sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHelperCheckContainer(t *testing.T) {
 | 
				
			||||||
 | 
						if len(os.Args) != 6 || os.Args[4] != "check" {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("user", func(t *testing.T) {
 | 
				
			||||||
 | 
							if uid := syscall.Getuid(); uid != 1000 {
 | 
				
			||||||
 | 
								t.Errorf("Getuid: %d, want 1000", uid)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if gid := syscall.Getgid(); gid != 100 {
 | 
				
			||||||
 | 
								t.Errorf("Getgid: %d, want 100", gid)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("hostname", func(t *testing.T) {
 | 
				
			||||||
 | 
							if name, err := os.Hostname(); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot get hostname: %v", err)
 | 
				
			||||||
 | 
							} else if name != os.Args[5] {
 | 
				
			||||||
 | 
								t.Errorf("Hostname: %q, want %q", name, os.Args[5])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if p, err := os.ReadFile("/etc/hostname"); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("%v", err)
 | 
				
			||||||
 | 
							} else if string(p) != os.Args[5] {
 | 
				
			||||||
 | 
								t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("mount", func(t *testing.T) {
 | 
				
			||||||
 | 
							var mnt []*vfs.MountInfoEntry
 | 
				
			||||||
 | 
							if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot receive expected mount points: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var d *vfs.MountInfoDecoder
 | 
				
			||||||
 | 
							if f, err := os.Open("/proc/self/mountinfo"); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("cannot open mountinfo: %v", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								d = vfs.NewMountInfoDecoder(f)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							i := 0
 | 
				
			||||||
 | 
							for cur := range d.Entries() {
 | 
				
			||||||
 | 
								if i == len(mnt) {
 | 
				
			||||||
 | 
									t.Errorf("got more than %d entries", len(mnt))
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
 | 
				
			||||||
 | 
								cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
 | 
				
			||||||
 | 
								cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
 | 
				
			||||||
 | 
								mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
 | 
				
			||||||
 | 
								mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !cur.EqualWithIgnore(mnt[i], "\x00") {
 | 
				
			||||||
 | 
									t.Errorf("[FAIL] %s", cur)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									t.Logf("[ OK ] %s", cur)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								i++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := d.Err(); err != nil {
 | 
				
			||||||
 | 
								t.Errorf("cannot parse mountinfo: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if i != len(mnt) {
 | 
				
			||||||
 | 
								t.Errorf("got %d entries, want %d", i, len(mnt))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func commandContext(ctx context.Context) *exec.Cmd {
 | 
				
			||||||
 | 
						return exec.CommandContext(ctx, os.Args[0], "-test.v",
 | 
				
			||||||
 | 
							"-test.run=TestHelperInit", "--", "init")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
package internal
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
@ -15,7 +13,7 @@ var (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func copyExecutable() {
 | 
					func copyExecutable() {
 | 
				
			||||||
	if name, err := os.Executable(); err != nil {
 | 
						if name, err := os.Executable(); err != nil {
 | 
				
			||||||
		fmsg.BeforeExit()
 | 
							msg.BeforeExit()
 | 
				
			||||||
		log.Fatalf("cannot read executable path: %v", err)
 | 
							log.Fatalf("cannot read executable path: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		executable = name
 | 
							executable = name
 | 
				
			||||||
							
								
								
									
										17
									
								
								sandbox/executable_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sandbox/executable_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package sandbox_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestExecutable(t *testing.T) {
 | 
				
			||||||
 | 
						for i := 0; i < 16; i++ {
 | 
				
			||||||
 | 
							if got := sandbox.MustExecutable(); got != os.Args[0] {
 | 
				
			||||||
 | 
								t.Errorf("MustExecutable: %q, want %q",
 | 
				
			||||||
 | 
									got, os.Args[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										378
									
								
								sandbox/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								sandbox/init.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,378 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// time to wait for linger processes after death of initial process
 | 
				
			||||||
 | 
						residualProcessTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// intermediate tmpfs mount point
 | 
				
			||||||
 | 
						basePath = "/tmp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// setup params file descriptor
 | 
				
			||||||
 | 
						setupEnv = "FORTIFY_SETUP"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type initParams struct {
 | 
				
			||||||
 | 
						Params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						HostUid, HostGid int
 | 
				
			||||||
 | 
						// extra files count
 | 
				
			||||||
 | 
						Count int
 | 
				
			||||||
 | 
						// verbosity pass through
 | 
				
			||||||
 | 
						Verbose bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			||||||
 | 
						runtime.LockOSThread()
 | 
				
			||||||
 | 
						prepare("init")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if os.Getpid() != 1 {
 | 
				
			||||||
 | 
							log.Fatal("this process must run as pid 1")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							receive setup payload
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							params      initParams
 | 
				
			||||||
 | 
							closeSetup  func() error
 | 
				
			||||||
 | 
							setupFile   *os.File
 | 
				
			||||||
 | 
							offsetSetup int
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, ErrInvalid) {
 | 
				
			||||||
 | 
								log.Fatal("invalid setup descriptor")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if errors.Is(err, ErrNotSet) {
 | 
				
			||||||
 | 
								log.Fatal("FORTIFY_SETUP not set")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Fatalf("cannot decode init setup payload: %v", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if params.Ops == nil {
 | 
				
			||||||
 | 
								log.Fatal("invalid setup parameters")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if params.ParentPerm == 0 {
 | 
				
			||||||
 | 
								params.ParentPerm = 0755
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							setVerbose(params.Verbose)
 | 
				
			||||||
 | 
							msg.Verbose("received setup parameters")
 | 
				
			||||||
 | 
							closeSetup = f
 | 
				
			||||||
 | 
							offsetSetup = int(setupFile.Fd() + 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// write uid/gid map here so parent does not need to set dumpable
 | 
				
			||||||
 | 
						if err := SetDumpable(SUID_DUMP_USER); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/uid_map",
 | 
				
			||||||
 | 
							append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
 | 
				
			||||||
 | 
							0); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/setgroups",
 | 
				
			||||||
 | 
							[]byte("deny\n"),
 | 
				
			||||||
 | 
							0); err != nil && !os.IsNotExist(err) {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.WriteFile("/proc/self/gid_map",
 | 
				
			||||||
 | 
							append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
 | 
				
			||||||
 | 
							0); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oldmask := syscall.Umask(0)
 | 
				
			||||||
 | 
						if params.Hostname != "" {
 | 
				
			||||||
 | 
							if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot set hostname: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// cache sysctl before pivot_root
 | 
				
			||||||
 | 
						LastCap()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							set up mount points from intermediate root
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount("", "/", "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot make / rslave: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, op := range *params.Ops {
 | 
				
			||||||
 | 
							if op == nil {
 | 
				
			||||||
 | 
								log.Fatalf("invalid op %d", i)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := op.early(¶ms.Params); err != nil {
 | 
				
			||||||
 | 
								msg.PrintBaseErr(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot prepare op %d:", i))
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount("rootfs", basePath, "tmpfs",
 | 
				
			||||||
 | 
							syscall.MS_NODEV|syscall.MS_NOSUID,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot mount intermediate root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.Chdir(basePath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot enter base path: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(sysrootDir, 0755); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.Mount(sysrootDir, sysrootDir, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot bind sysroot: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(hostDir, 0755); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.PivotRoot(basePath, hostDir); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot pivot into intermediate root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.Chdir("/"); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, op := range *params.Ops {
 | 
				
			||||||
 | 
							// ops already checked during early setup
 | 
				
			||||||
 | 
							msg.Verbosef("%s %s", op.prefix(), op)
 | 
				
			||||||
 | 
							if err := op.apply(¶ms.Params); err != nil {
 | 
				
			||||||
 | 
								msg.PrintBaseErr(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot apply op %d:", i))
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							pivot to sysroot
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount(hostDir, hostDir, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
 | 
				
			||||||
 | 
							""); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot make host root rprivate: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot unmount host root: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							var fd int
 | 
				
			||||||
 | 
							if err := IgnoringEINTR(func() (err error) {
 | 
				
			||||||
 | 
								fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot open intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := os.Chdir(sysrootPath); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("%v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := syscall.PivotRoot(".", "."); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot pivot into sysroot: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := syscall.Fchdir(fd); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot re-enter intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot unmount intemediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := os.Chdir("/"); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("%v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := syscall.Close(fd); err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("cannot close intermediate root: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							caps/securebits and seccomp filter
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
 | 
				
			||||||
 | 
							log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
 | 
				
			||||||
 | 
							log.Fatalf("cannot clear the ambient capability set: %v", errno)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i := uintptr(0); i <= LastCap(); i++ {
 | 
				
			||||||
 | 
							if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
 | 
				
			||||||
 | 
								log.Fatalf("cannot drop capability from bonding set: %v", errno)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := capset(
 | 
				
			||||||
 | 
							&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
 | 
				
			||||||
 | 
							&[2]capData{{0, 0, 0}, {0, 0, 0}},
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot capset: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot load syscall filter: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							pass through extra files
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						extraFiles := make([]*os.File, params.Count)
 | 
				
			||||||
 | 
						for i := range extraFiles {
 | 
				
			||||||
 | 
							extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						syscall.Umask(oldmask)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							prepare initial process
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd := exec.Command(params.Path)
 | 
				
			||||||
 | 
						cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
 | 
						cmd.Args = params.Args
 | 
				
			||||||
 | 
						cmd.Env = params.Env
 | 
				
			||||||
 | 
						cmd.ExtraFiles = extraFiles
 | 
				
			||||||
 | 
						cmd.Dir = params.Dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := cmd.Start(); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("%v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						msg.Suspend()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							close setup pipe
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := closeSetup(); err != nil {
 | 
				
			||||||
 | 
							log.Println("cannot close setup pipe:", err)
 | 
				
			||||||
 | 
							// not fatal
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							perform init duties
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sig := make(chan os.Signal, 2)
 | 
				
			||||||
 | 
						signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type winfo struct {
 | 
				
			||||||
 | 
							wpid    int
 | 
				
			||||||
 | 
							wstatus syscall.WaitStatus
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						info := make(chan winfo, 1)
 | 
				
			||||||
 | 
						done := make(chan struct{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								err     error
 | 
				
			||||||
 | 
								wpid    = -2
 | 
				
			||||||
 | 
								wstatus syscall.WaitStatus
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// keep going until no child process is left
 | 
				
			||||||
 | 
							for wpid != -1 {
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if wpid != -2 {
 | 
				
			||||||
 | 
									info <- winfo{wpid, wstatus}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = syscall.EINTR
 | 
				
			||||||
 | 
								for errors.Is(err, syscall.EINTR) {
 | 
				
			||||||
 | 
									wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !errors.Is(err, syscall.ECHILD) {
 | 
				
			||||||
 | 
								log.Println("unexpected wait4 response:", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							close(done)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// closed after residualProcessTimeout has elapsed after initial process death
 | 
				
			||||||
 | 
						timeout := make(chan struct{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						r := 2
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case s := <-sig:
 | 
				
			||||||
 | 
								if msg.Resume() {
 | 
				
			||||||
 | 
									msg.Verbosef("terminating on %s after process start", s.String())
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									msg.Verbosef("terminating on %s", s.String())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(0)
 | 
				
			||||||
 | 
							case w := <-info:
 | 
				
			||||||
 | 
								if w.wpid == cmd.Process.Pid {
 | 
				
			||||||
 | 
									// initial process exited, output is most likely available again
 | 
				
			||||||
 | 
									msg.Resume()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									switch {
 | 
				
			||||||
 | 
									case w.wstatus.Exited():
 | 
				
			||||||
 | 
										r = w.wstatus.ExitStatus()
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
 | 
				
			||||||
 | 
									case w.wstatus.Signaled():
 | 
				
			||||||
 | 
										r = 128 + int(w.wstatus.Signal())
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
 | 
				
			||||||
 | 
									default:
 | 
				
			||||||
 | 
										r = 255
 | 
				
			||||||
 | 
										msg.Verbosef("initial process exited with status %#x", w.wstatus)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									go func() {
 | 
				
			||||||
 | 
										time.Sleep(residualProcessTimeout)
 | 
				
			||||||
 | 
										close(timeout)
 | 
				
			||||||
 | 
									}()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case <-done:
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(r)
 | 
				
			||||||
 | 
							case <-timeout:
 | 
				
			||||||
 | 
								log.Println("timeout exceeded waiting for lingering processes")
 | 
				
			||||||
 | 
								msg.BeforeExit()
 | 
				
			||||||
 | 
								os.Exit(r)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TryArgv0 calls [Init] if the last element of argv0 is "init".
 | 
				
			||||||
 | 
					func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
 | 
				
			||||||
 | 
						if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
 | 
				
			||||||
 | 
							msg = v
 | 
				
			||||||
 | 
							Init(prepare, setVerbose)
 | 
				
			||||||
 | 
							msg.BeforeExit()
 | 
				
			||||||
 | 
							os.Exit(0)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										125
									
								
								sandbox/mount.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								sandbox/mount.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,125 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
 | 
				
			||||||
 | 
						if eq {
 | 
				
			||||||
 | 
							msg.Verbosef("resolved %q flags %#x", target, flags)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount(source, target, "",
 | 
				
			||||||
 | 
							syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot mount %q on %q:", source, target))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var targetFinal string
 | 
				
			||||||
 | 
						if v, err := filepath.EvalSymlinks(target); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							targetFinal = v
 | 
				
			||||||
 | 
							if targetFinal != target {
 | 
				
			||||||
 | 
								msg.Verbosef("target resolves to %q", targetFinal)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// final target path according to the kernel through proc
 | 
				
			||||||
 | 
						var targetKFinal string
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							var destFd int
 | 
				
			||||||
 | 
							if err := IgnoringEINTR(func() (err error) {
 | 
				
			||||||
 | 
								destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot open %q:", targetFinal))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if v, err := os.Readlink(p.fd(destFd)); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							} else if err = syscall.Close(destFd); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									fmt.Sprintf("cannot close %q:", targetFinal))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								targetKFinal = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
 | 
				
			||||||
 | 
						return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
 | 
				
			||||||
 | 
							n, err := d.Unfold(targetKFinal)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, syscall.ESTALE) {
 | 
				
			||||||
 | 
									return msg.WrapErr(err,
 | 
				
			||||||
 | 
										fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot unfold mount hierarchy:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err = remountWithFlags(n, mf); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if flags&syscall.MS_REC == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for cur := range n.Collective() {
 | 
				
			||||||
 | 
								err = remountWithFlags(cur, mf)
 | 
				
			||||||
 | 
								if err != nil && !errors.Is(err, syscall.EACCES) {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
 | 
				
			||||||
 | 
						kf, unmatched := n.Flags()
 | 
				
			||||||
 | 
						if len(unmatched) != 0 {
 | 
				
			||||||
 | 
							msg.Verbosef("unmatched vfs options: %q", unmatched)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if kf&mf != mf {
 | 
				
			||||||
 | 
							return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
 | 
				
			||||||
 | 
								syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
 | 
				
			||||||
 | 
								""),
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot remount %q:", n.Clean))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
 | 
				
			||||||
 | 
						target := toSysroot(name)
 | 
				
			||||||
 | 
						if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						opt := fmt.Sprintf("mode=%#o", perm)
 | 
				
			||||||
 | 
						if size > 0 {
 | 
				
			||||||
 | 
							opt += fmt.Sprintf(",size=%d", size)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
 | 
				
			||||||
 | 
							syscall.MS_NOSUID|syscall.MS_NODEV, opt),
 | 
				
			||||||
 | 
							fmt.Sprintf("cannot mount tmpfs on %q:", name))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parentPerm(perm os.FileMode) os.FileMode {
 | 
				
			||||||
 | 
						pperm := 0755
 | 
				
			||||||
 | 
						if perm&0070 == 0 {
 | 
				
			||||||
 | 
							pperm &= ^0050
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if perm&0007 == 0 {
 | 
				
			||||||
 | 
							pperm &= ^0005
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return os.FileMode(pperm)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								sandbox/msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								sandbox/msg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Msg interface {
 | 
				
			||||||
 | 
						IsVerbose() bool
 | 
				
			||||||
 | 
						Verbose(v ...any)
 | 
				
			||||||
 | 
						Verbosef(format string, v ...any)
 | 
				
			||||||
 | 
						WrapErr(err error, a ...any) error
 | 
				
			||||||
 | 
						PrintBaseErr(err error, fallback string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Suspend()
 | 
				
			||||||
 | 
						Resume() bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						BeforeExit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DefaultMsg struct{ inactive atomic.Bool }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) IsVerbose() bool { return true }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Verbose(v ...any) {
 | 
				
			||||||
 | 
						if !msg.inactive.Load() {
 | 
				
			||||||
 | 
							log.Println(v...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Verbosef(format string, v ...any) {
 | 
				
			||||||
 | 
						if !msg.inactive.Load() {
 | 
				
			||||||
 | 
							log.Printf(format, v...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
 | 
				
			||||||
 | 
						log.Println(a...)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Suspend()     { msg.inactive.Store(true) }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
 | 
				
			||||||
 | 
					func (msg *DefaultMsg) BeforeExit()  {}
 | 
				
			||||||
							
								
								
									
										26
									
								
								sandbox/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								sandbox/output.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var msg Msg = new(DefaultMsg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetOutput() Msg { return msg }
 | 
				
			||||||
 | 
					func SetOutput(v Msg) {
 | 
				
			||||||
 | 
						if v == nil {
 | 
				
			||||||
 | 
							msg = new(DefaultMsg)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func wrapErrSuffix(err error, a ...any) error {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return msg.WrapErr(err, append(a, err)...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func wrapErrSelf(err error) error {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return msg.WrapErr(err, err.Error())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package proc
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
@ -12,7 +12,7 @@ var (
 | 
				
			|||||||
	ErrInvalid = errors.New("bad file descriptor")
 | 
						ErrInvalid = errors.New("bad file descriptor")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Setup appends the read end of a pipe for payload transmission and returns its fd.
 | 
					// Setup appends the read end of a pipe for setup params transmission and returns its fd.
 | 
				
			||||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
					func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
				
			||||||
	if r, w, err := os.Pipe(); err != nil {
 | 
						if r, w, err := os.Pipe(); err != nil {
 | 
				
			||||||
		return -1, nil, err
 | 
							return -1, nil, err
 | 
				
			||||||
@ -23,9 +23,8 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Receive retrieves payload pipe fd from the environment,
 | 
					// Receive retrieves setup fd from the environment and receives params.
 | 
				
			||||||
// receives its payload and returns the Close method of the pipe.
 | 
					func Receive(key string, e any, v **os.File) (func() error, error) {
 | 
				
			||||||
func Receive(key string, e any) (func() error, error) {
 | 
					 | 
				
			||||||
	var setup *os.File
 | 
						var setup *os.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s, ok := os.LookupEnv(key); !ok {
 | 
						if s, ok := os.LookupEnv(key); !ok {
 | 
				
			||||||
@ -38,8 +37,11 @@ func Receive(key string, e any) (func() error, error) {
 | 
				
			|||||||
			if setup == nil {
 | 
								if setup == nil {
 | 
				
			||||||
				return nil, ErrInvalid
 | 
									return nil, ErrInvalid
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if v != nil {
 | 
				
			||||||
 | 
									*v = setup
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
 | 
						return setup.Close, gob.NewDecoder(setup).Decode(e)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										94
									
								
								sandbox/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								sandbox/path.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/vfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						hostPath    = "/" + hostDir
 | 
				
			||||||
 | 
						hostDir     = "host"
 | 
				
			||||||
 | 
						sysrootPath = "/" + sysrootDir
 | 
				
			||||||
 | 
						sysrootDir  = "sysroot"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toSysroot(name string) string {
 | 
				
			||||||
 | 
						name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
 | 
				
			||||||
 | 
						return path.Join(sysrootPath, name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toHost(name string) string {
 | 
				
			||||||
 | 
						name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
 | 
				
			||||||
 | 
						return path.Join(hostPath, name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createFile(name string, perm, pperm os.FileMode, content []byte) error {
 | 
				
			||||||
 | 
						if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if content != nil {
 | 
				
			||||||
 | 
							_, err = f.Write(content)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return errors.Join(f.Close(), err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ensureFile(name string, perm, pperm os.FileMode) error {
 | 
				
			||||||
 | 
						fi, err := os.Stat(name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if !os.IsNotExist(err) {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return createFile(name, perm, pperm, nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
 | 
				
			||||||
 | 
							err = msg.WrapErr(syscall.EISDIR,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is a directory", name))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var hostProc = newProcPats(hostPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newProcPats(prefix string) *procPaths {
 | 
				
			||||||
 | 
						return &procPaths{prefix + "/proc", prefix + "/proc/self"}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type procPaths struct {
 | 
				
			||||||
 | 
						prefix string
 | 
				
			||||||
 | 
						self   string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *procPaths) stdout() string   { return p.self + "/fd/1" }
 | 
				
			||||||
 | 
					func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
 | 
				
			||||||
 | 
					func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
 | 
				
			||||||
 | 
						if r, err := os.Open(p.self + "/mountinfo"); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							d := vfs.NewMountInfoDecoder(r)
 | 
				
			||||||
 | 
							err0 := f(d)
 | 
				
			||||||
 | 
							if err = r.Close(); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot close mountinfo:")
 | 
				
			||||||
 | 
							} else if err = d.Err(); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSuffix(err,
 | 
				
			||||||
 | 
									"cannot parse mountinfo:")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return err0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -11,6 +11,9 @@ import (
 | 
				
			|||||||
// New returns an inactive Encoder instance.
 | 
					// New returns an inactive Encoder instance.
 | 
				
			||||||
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
 | 
					func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load loads a filter into the kernel.
 | 
				
			||||||
 | 
					func Load(opts SyscallOpts) error { return buildFilter(-1, opts) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
An Encoder writes a BPF program to an output stream.
 | 
					An Encoder writes a BPF program to an output stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,7 +28,7 @@ func (e *exporter) prepare() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		ec := make(chan error, 1)
 | 
							ec := make(chan error, 1)
 | 
				
			||||||
		go func(fd uintptr) {
 | 
							go func(fd uintptr) {
 | 
				
			||||||
			ec <- exportFilter(fd, e.opts)
 | 
								ec <- buildFilter(int(fd), e.opts)
 | 
				
			||||||
			close(ec)
 | 
								close(ec)
 | 
				
			||||||
			_ = e.closeWrite()
 | 
								_ = e.closeWrite()
 | 
				
			||||||
			runtime.KeepAlive(e.w)
 | 
								runtime.KeepAlive(e.w)
 | 
				
			||||||
@ -4,12 +4,11 @@ import (
 | 
				
			|||||||
	"crypto/sha512"
 | 
						"crypto/sha512"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/seccomp"
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestExport(t *testing.T) {
 | 
					func TestExport(t *testing.T) {
 | 
				
			||||||
@ -79,8 +78,9 @@ func TestExport(t *testing.T) {
 | 
				
			|||||||
	buf := make([]byte, 8)
 | 
						buf := make([]byte, 8)
 | 
				
			||||||
	for _, tc := range testCases {
 | 
						for _, tc := range testCases {
 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			seccomp.CPrintln = log.Println
 | 
								oldF := seccomp.GetOutput()
 | 
				
			||||||
			t.Cleanup(func() { seccomp.CPrintln = nil })
 | 
								seccomp.SetOutput(t.Log)
 | 
				
			||||||
 | 
								t.Cleanup(func() { seccomp.SetOutput(oldF) })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			e := seccomp.New(tc.opts)
 | 
								e := seccomp.New(tc.opts)
 | 
				
			||||||
			digest := sha512.New()
 | 
								digest := sha512.New()
 | 
				
			||||||
@ -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 || err.Error() != "seccomp_export_bpf failed: operation canceled" {
 | 
							// 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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
							
								
								
									
										30
									
								
								sandbox/seccomp/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								sandbox/seccomp/output.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					package seccomp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "C"
 | 
				
			||||||
 | 
					import "sync/atomic"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var printlnP atomic.Pointer[func(v ...any)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SetOutput(f func(v ...any)) {
 | 
				
			||||||
 | 
						if f == nil {
 | 
				
			||||||
 | 
							// avoid storing nil function
 | 
				
			||||||
 | 
							printlnP.Store(nil)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							printlnP.Store(&f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetOutput() func(v ...any) {
 | 
				
			||||||
 | 
						if fp := printlnP.Load(); fp == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return *fp
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//export F_println
 | 
				
			||||||
 | 
					func F_println(v *C.char) {
 | 
				
			||||||
 | 
						if fp := printlnP.Load(); fp != nil {
 | 
				
			||||||
 | 
							(*fp)(C.GoString(v))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
#define _GNU_SOURCE // CLONE_NEWUSER
 | 
					#define _GNU_SOURCE // CLONE_NEWUSER
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "seccomp-export.h"
 | 
					#include "seccomp-build.h"
 | 
				
			||||||
#include <stdlib.h>
 | 
					#include <stdlib.h>
 | 
				
			||||||
#include <stdio.h>
 | 
					#include <stdio.h>
 | 
				
			||||||
#include <assert.h>
 | 
					#include <assert.h>
 | 
				
			||||||
@ -27,28 +27,27 @@ struct f_syscall_act {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
 | 
					#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define SECCOMP_RULESET_ADD(ruleset) do {                                                                      \
 | 
					#define SECCOMP_RULESET_ADD(ruleset) do {                                                                         \
 | 
				
			||||||
  if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\"");                                  \
 | 
					  if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\"");                                     \
 | 
				
			||||||
  for (int i = 0; i < LEN(ruleset); i++) {                                                                     \
 | 
					  for (int i = 0; i < LEN(ruleset); i++) {                                                                        \
 | 
				
			||||||
    assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS);                                       \
 | 
					    assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS);                                          \
 | 
				
			||||||
                                                                                                               \
 | 
					                                                                                                                  \
 | 
				
			||||||
    if (ruleset[i].arg)                                                                                        \
 | 
					    if (ruleset[i].arg)                                                                                           \
 | 
				
			||||||
      ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
 | 
					      *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
 | 
				
			||||||
    else                                                                                                       \
 | 
					    else                                                                                                          \
 | 
				
			||||||
      ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0);                  \
 | 
					      *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0);                  \
 | 
				
			||||||
                                                                                                               \
 | 
					                                                                                                                  \
 | 
				
			||||||
    if (ret == -EFAULT) {                                                                                      \
 | 
					    if (*ret_p == -EFAULT) {                                                                                      \
 | 
				
			||||||
      res = 4;                                                                                                 \
 | 
					      res = 4;                                                                                                    \
 | 
				
			||||||
      goto out;                                                                                                \
 | 
					      goto out;                                                                                                   \
 | 
				
			||||||
    } else if (ret < 0) {                                                                                      \
 | 
					    } else if (*ret_p < 0) {                                                                                      \
 | 
				
			||||||
      res = 5;                                                                                                 \
 | 
					      res = 5;                                                                                                    \
 | 
				
			||||||
      errno = -ret;                                                                                            \
 | 
					      goto out;                                                                                                   \
 | 
				
			||||||
      goto out;                                                                                                \
 | 
					    }                                                                                                             \
 | 
				
			||||||
    }                                                                                                          \
 | 
					  }                                                                                                               \
 | 
				
			||||||
  }                                                                                                            \
 | 
					 | 
				
			||||||
} while (0)
 | 
					} while (0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
 | 
					int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
 | 
				
			||||||
  int32_t res = 0; // refer to resErr for meaning
 | 
					  int32_t res = 0; // refer to resErr for meaning
 | 
				
			||||||
  int allow_multiarch = opts & F_MULTIARCH;
 | 
					  int allow_multiarch = opts & F_MULTIARCH;
 | 
				
			||||||
  int allowed_personality = PER_LINUX;
 | 
					  int allowed_personality = PER_LINUX;
 | 
				
			||||||
@ -229,8 +228,6 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
 | 
				
			|||||||
  } else
 | 
					  } else
 | 
				
			||||||
    errno = 0;
 | 
					    errno = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int ret;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // We only really need to handle arches on multiarch systems.
 | 
					  // We only really need to handle arches on multiarch systems.
 | 
				
			||||||
  // If only one arch is supported the default is fine
 | 
					  // If only one arch is supported the default is fine
 | 
				
			||||||
  if (arch != 0) {
 | 
					  if (arch != 0) {
 | 
				
			||||||
@ -239,18 +236,16 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
 | 
				
			|||||||
    // allow the target arch, but we can't really disallow the
 | 
					    // allow the target arch, but we can't really disallow the
 | 
				
			||||||
    // native arch at this point, because then bubblewrap
 | 
					    // native arch at this point, because then bubblewrap
 | 
				
			||||||
    // couldn't continue running.
 | 
					    // couldn't continue running.
 | 
				
			||||||
    ret = seccomp_arch_add(ctx, arch);
 | 
					    *ret_p = seccomp_arch_add(ctx, arch);
 | 
				
			||||||
    if (ret < 0 && ret != -EEXIST) {
 | 
					    if (*ret_p < 0 && *ret_p != -EEXIST) {
 | 
				
			||||||
      res = 2;
 | 
					      res = 2;
 | 
				
			||||||
      errno = -ret;
 | 
					 | 
				
			||||||
      goto out;
 | 
					      goto out;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (allow_multiarch && multiarch != 0) {
 | 
					    if (allow_multiarch && multiarch != 0) {
 | 
				
			||||||
      ret = seccomp_arch_add(ctx, multiarch);
 | 
					      *ret_p = seccomp_arch_add(ctx, multiarch);
 | 
				
			||||||
      if (ret < 0 && ret != -EEXIST) {
 | 
					      if (*ret_p < 0 && *ret_p != -EEXIST) {
 | 
				
			||||||
        res = 3;
 | 
					        res = 3;
 | 
				
			||||||
        errno = -ret;
 | 
					 | 
				
			||||||
        goto out;
 | 
					        goto out;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -285,11 +280,18 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
 | 
				
			|||||||
  // Blocklist the rest
 | 
					  // Blocklist the rest
 | 
				
			||||||
  seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
 | 
					  seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ret = seccomp_export_bpf(ctx, fd);
 | 
					  if (fd < 0) {
 | 
				
			||||||
  if (ret != 0) {
 | 
					    *ret_p = seccomp_load(ctx);
 | 
				
			||||||
    res = 6;
 | 
					    if (*ret_p != 0) {
 | 
				
			||||||
    errno = -ret;
 | 
					      res = 7;
 | 
				
			||||||
    goto out;
 | 
					      goto out;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    *ret_p = seccomp_export_bpf(ctx, fd);
 | 
				
			||||||
 | 
					    if (*ret_p != 0) {
 | 
				
			||||||
 | 
					      res = 6;
 | 
				
			||||||
 | 
					      goto out;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
out:
 | 
					out:
 | 
				
			||||||
@ -20,4 +20,4 @@ typedef enum {
 | 
				
			|||||||
} f_syscall_opts;
 | 
					} f_syscall_opts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
extern void F_println(char *v);
 | 
					extern void F_println(char *v);
 | 
				
			||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
 | 
					int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
 | 
				
			||||||
@ -3,25 +3,56 @@ package seccomp
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
#cgo linux pkg-config: --static libseccomp
 | 
					#cgo linux pkg-config: --static libseccomp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "seccomp-export.h"
 | 
					#include "seccomp-build.h"
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
import "C"
 | 
					import "C"
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"runtime"
 | 
						"runtime"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var CPrintln func(v ...any)
 | 
					// LibraryError represents a libseccomp error.
 | 
				
			||||||
 | 
					type LibraryError struct {
 | 
				
			||||||
 | 
						Prefix  string
 | 
				
			||||||
 | 
						Seccomp syscall.Errno
 | 
				
			||||||
 | 
						Errno   error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var resErr = [...]error{
 | 
					func (e *LibraryError) Error() string {
 | 
				
			||||||
	0: nil,
 | 
						if e.Seccomp == 0 {
 | 
				
			||||||
	1: errors.New("seccomp_init failed"),
 | 
							if e.Errno == nil {
 | 
				
			||||||
	2: errors.New("seccomp_arch_add failed"),
 | 
								panic("invalid libseccomp error")
 | 
				
			||||||
	3: errors.New("seccomp_arch_add failed (multiarch)"),
 | 
							}
 | 
				
			||||||
	4: errors.New("internal libseccomp failure"),
 | 
							return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
 | 
				
			||||||
	5: errors.New("seccomp_rule_add failed"),
 | 
						}
 | 
				
			||||||
	6: errors.New("seccomp_export_bpf failed"),
 | 
						if e.Errno == nil {
 | 
				
			||||||
 | 
							return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *LibraryError) Is(err error) bool {
 | 
				
			||||||
 | 
						if e == nil {
 | 
				
			||||||
 | 
							return err == nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ef, ok := err.(*LibraryError); ok {
 | 
				
			||||||
 | 
							return *e == *ef
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
 | 
				
			||||||
 | 
							(e.Errno != nil && errors.Is(err, e.Errno))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var resPrefix = [...]string{
 | 
				
			||||||
 | 
						0: "",
 | 
				
			||||||
 | 
						1: "seccomp_init failed",
 | 
				
			||||||
 | 
						2: "seccomp_arch_add failed",
 | 
				
			||||||
 | 
						3: "seccomp_arch_add failed (multiarch)",
 | 
				
			||||||
 | 
						4: "internal libseccomp failure",
 | 
				
			||||||
 | 
						5: "seccomp_rule_add failed",
 | 
				
			||||||
 | 
						6: "seccomp_export_bpf failed",
 | 
				
			||||||
 | 
						7: "seccomp_load failed",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SyscallOpts = C.f_syscall_opts
 | 
					type SyscallOpts = C.f_syscall_opts
 | 
				
			||||||
@ -46,7 +77,7 @@ const (
 | 
				
			|||||||
	FlagBluetooth SyscallOpts = C.F_BLUETOOTH
 | 
						FlagBluetooth SyscallOpts = C.F_BLUETOOTH
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func exportFilter(fd uintptr, opts SyscallOpts) error {
 | 
					func buildFilter(fd int, opts SyscallOpts) error {
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		arch      C.uint32_t = 0
 | 
							arch      C.uint32_t = 0
 | 
				
			||||||
		multiarch C.uint32_t = 0
 | 
							multiarch C.uint32_t = 0
 | 
				
			||||||
@ -66,23 +97,18 @@ func exportFilter(fd uintptr, opts SyscallOpts) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// this removes repeated transitions between C and Go execution
 | 
						// this removes repeated transitions between C and Go execution
 | 
				
			||||||
	// when producing log output via F_println and CPrintln is nil
 | 
						// when producing log output via F_println and CPrintln is nil
 | 
				
			||||||
	if CPrintln != nil {
 | 
						if fp := printlnP.Load(); fp != nil {
 | 
				
			||||||
		opts |= flagVerbose
 | 
							opts |= flagVerbose
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
 | 
						var ret C.int
 | 
				
			||||||
	if re := resErr[res]; re != nil {
 | 
						res, err := C.f_build_filter(&ret, C.int(fd), arch, multiarch, opts)
 | 
				
			||||||
		if err == nil {
 | 
						if prefix := resPrefix[res]; prefix != "" {
 | 
				
			||||||
			return re
 | 
							return &LibraryError{
 | 
				
			||||||
 | 
								prefix,
 | 
				
			||||||
 | 
								-syscall.Errno(ret),
 | 
				
			||||||
 | 
								err,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return fmt.Errorf("%s: %v", re.Error(), err)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
//export F_println
 | 
					 | 
				
			||||||
func F_println(v *C.char) {
 | 
					 | 
				
			||||||
	if CPrintln != nil {
 | 
					 | 
				
			||||||
		CPrintln(C.GoString(v))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										65
									
								
								sandbox/seccomp/seccomp_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								sandbox/seccomp/seccomp_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					package seccomp_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/sandbox/seccomp"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestLibraryError(t *testing.T) {
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name    string
 | 
				
			||||||
 | 
							sample  *seccomp.LibraryError
 | 
				
			||||||
 | 
							want    string
 | 
				
			||||||
 | 
							wantIs  bool
 | 
				
			||||||
 | 
							compare error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"full",
 | 
				
			||||||
 | 
								&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
 | 
				
			||||||
 | 
								"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
 | 
				
			||||||
 | 
								true,
 | 
				
			||||||
 | 
								&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"errno only",
 | 
				
			||||||
 | 
								&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
 | 
				
			||||||
 | 
								"seccomp_init failed: cannot allocate memory",
 | 
				
			||||||
 | 
								false,
 | 
				
			||||||
 | 
								nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"seccomp only",
 | 
				
			||||||
 | 
								&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
 | 
				
			||||||
 | 
								"internal libseccomp failure: bad address",
 | 
				
			||||||
 | 
								true,
 | 
				
			||||||
 | 
								syscall.EFAULT,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								if errors.Is(tc.sample, tc.compare) != tc.wantIs {
 | 
				
			||||||
 | 
									t.Errorf("errors.Is(%#v, %#v) did not return %v",
 | 
				
			||||||
 | 
										tc.sample, tc.compare, tc.wantIs)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if got := tc.sample.Error(); got != tc.want {
 | 
				
			||||||
 | 
									t.Errorf("Error: %q, want %q",
 | 
				
			||||||
 | 
										got, tc.want)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("invalid", func(t *testing.T) {
 | 
				
			||||||
 | 
							wantPanic := "invalid libseccomp error"
 | 
				
			||||||
 | 
							defer func() {
 | 
				
			||||||
 | 
								if r := recover(); r != wantPanic {
 | 
				
			||||||
 | 
									t.Errorf("panic: %q, want %q", r, wantPanic)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
							runtime.KeepAlive(new(seccomp.LibraryError).Error())
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										426
									
								
								sandbox/sequential.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										426
									
								
								sandbox/sequential.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,426 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"math"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"unsafe"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(BindMount)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BindMount bind mounts host path Source on container path Target.
 | 
				
			||||||
 | 
					type BindMount struct {
 | 
				
			||||||
 | 
						Source, SourceFinal, Target string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Flags int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						BindOptional = 1 << iota
 | 
				
			||||||
 | 
						BindWritable
 | 
				
			||||||
 | 
						BindDevice
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *BindMount) early(*Params) error {
 | 
				
			||||||
 | 
						if !path.IsAbs(b.Source) {
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is not absolute", b.Source))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if v, err := filepath.EvalSymlinks(b.Source); err != nil {
 | 
				
			||||||
 | 
							if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
 | 
				
			||||||
 | 
								b.SourceFinal = "\x00"
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} 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,
 | 
				
			||||||
 | 
								"path is not absolute")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						source := toHost(b.SourceFinal)
 | 
				
			||||||
 | 
						target := toSysroot(b.Target)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
 | 
				
			||||||
 | 
						// op->perms which is never set for any bind setup op so always results in 0700
 | 
				
			||||||
 | 
						if fi, err := os.Stat(source); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						} else if fi.IsDir() {
 | 
				
			||||||
 | 
							if err = os.MkdirAll(target, 0700); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} 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 (*BindMount) prefix() string  { return "mounting" }
 | 
				
			||||||
 | 
					func (b *BindMount) String() string {
 | 
				
			||||||
 | 
						if b.Source == b.Target {
 | 
				
			||||||
 | 
							return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (f *Ops) Bind(source, target string, flags int) *Ops {
 | 
				
			||||||
 | 
						*f = append(*f, &BindMount{source, "", target, flags})
 | 
				
			||||||
 | 
						return f
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(MountProc)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MountProc mounts a private instance of proc.
 | 
				
			||||||
 | 
					type MountProc string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p MountProc) early(*Params) error { return nil }
 | 
				
			||||||
 | 
					func (p MountProc) apply(params *Params) error {
 | 
				
			||||||
 | 
						v := string(p)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return wrapErrSuffix(syscall.Mount("proc", target, "proc",
 | 
				
			||||||
 | 
							syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
 | 
				
			||||||
 | 
							fmt.Sprintf("cannot mount proc on %q:", v))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p MountProc) Is(op Op) bool  { vp, ok := op.(MountProc); return ok && p == vp }
 | 
				
			||||||
 | 
					func (MountProc) prefix() string   { return "mounting" }
 | 
				
			||||||
 | 
					func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
 | 
				
			||||||
 | 
					func (f *Ops) Proc(dest string) *Ops {
 | 
				
			||||||
 | 
						*f = append(*f, MountProc(dest))
 | 
				
			||||||
 | 
						return f
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(MountDev)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MountDev mounts part of host dev.
 | 
				
			||||||
 | 
					type MountDev string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (d MountDev) early(*Params) error { return nil }
 | 
				
			||||||
 | 
					func (d MountDev) apply(params *Params) error {
 | 
				
			||||||
 | 
						v := string(d)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !path.IsAbs(v) {
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is not absolute", v))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						target := toSysroot(v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
 | 
				
			||||||
 | 
							targetPath := toSysroot(path.Join(v, name))
 | 
				
			||||||
 | 
							if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := hostProc.bindMount(
 | 
				
			||||||
 | 
								toHost("/dev/"+name),
 | 
				
			||||||
 | 
								targetPath,
 | 
				
			||||||
 | 
								0,
 | 
				
			||||||
 | 
								true,
 | 
				
			||||||
 | 
							); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i, name := range []string{"stdin", "stdout", "stderr"} {
 | 
				
			||||||
 | 
							if err := os.Symlink(
 | 
				
			||||||
 | 
								"/proc/self/fd/"+string(rune(i+'0')),
 | 
				
			||||||
 | 
								path.Join(target, name),
 | 
				
			||||||
 | 
							); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, pair := range [][2]string{
 | 
				
			||||||
 | 
							{"/proc/self/fd", "fd"},
 | 
				
			||||||
 | 
							{"/proc/kcore", "core"},
 | 
				
			||||||
 | 
							{"pts/ptmx", "ptmx"},
 | 
				
			||||||
 | 
						} {
 | 
				
			||||||
 | 
							if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						devPtsPath := path.Join(target, "pts")
 | 
				
			||||||
 | 
						for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
 | 
				
			||||||
 | 
							if err := os.Mkdir(name, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := syscall.Mount("devpts", devPtsPath, "devpts",
 | 
				
			||||||
 | 
							syscall.MS_NOSUID|syscall.MS_NOEXEC,
 | 
				
			||||||
 | 
							"newinstance,ptmxmode=0666,mode=620"); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSuffix(err,
 | 
				
			||||||
 | 
								fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if params.Flags&FAllowTTY != 0 {
 | 
				
			||||||
 | 
							var buf [8]byte
 | 
				
			||||||
 | 
							if _, _, errno := syscall.Syscall(
 | 
				
			||||||
 | 
								syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
 | 
				
			||||||
 | 
								uintptr(unsafe.Pointer(&buf[0])),
 | 
				
			||||||
 | 
							); errno == 0 {
 | 
				
			||||||
 | 
								consolePath := toSysroot(path.Join(v, "console"))
 | 
				
			||||||
 | 
								if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if name, err := os.Readlink(hostProc.stdout()); err != nil {
 | 
				
			||||||
 | 
									return wrapErrSelf(err)
 | 
				
			||||||
 | 
								} else if err = hostProc.bindMount(
 | 
				
			||||||
 | 
									toHost(name),
 | 
				
			||||||
 | 
									consolePath,
 | 
				
			||||||
 | 
									0,
 | 
				
			||||||
 | 
									false,
 | 
				
			||||||
 | 
								); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (d MountDev) Is(op Op) bool  { vd, ok := op.(MountDev); return ok && d == vd }
 | 
				
			||||||
 | 
					func (MountDev) prefix() string   { return "mounting" }
 | 
				
			||||||
 | 
					func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
 | 
				
			||||||
 | 
					func (f *Ops) Dev(dest string) *Ops {
 | 
				
			||||||
 | 
						*f = append(*f, MountDev(dest))
 | 
				
			||||||
 | 
						return f
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(MountMqueue)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MountMqueue mounts a private mqueue instance on container Path.
 | 
				
			||||||
 | 
					type MountMqueue string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m MountMqueue) early(*Params) error { return nil }
 | 
				
			||||||
 | 
					func (m MountMqueue) apply(params *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, params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(MountTmpfs)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MountTmpfs mounts tmpfs on container Path.
 | 
				
			||||||
 | 
					type MountTmpfs struct {
 | 
				
			||||||
 | 
						Path string
 | 
				
			||||||
 | 
						Size int
 | 
				
			||||||
 | 
						Perm os.FileMode
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *MountTmpfs) early(*Params) error { return nil }
 | 
				
			||||||
 | 
					func (t *MountTmpfs) apply(*Params) error {
 | 
				
			||||||
 | 
						if !path.IsAbs(t.Path) {
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
								fmt.Sprintf("path %q is not absolute", t.Path))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if t.Size < 0 || t.Size > math.MaxUint>>1 {
 | 
				
			||||||
 | 
							return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
								fmt.Sprintf("size %d out of bounds", t.Size))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *MountTmpfs) Is(op Op) bool  { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
 | 
				
			||||||
 | 
					func (*MountTmpfs) prefix() string   { return "mounting" }
 | 
				
			||||||
 | 
					func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
 | 
				
			||||||
 | 
					func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
 | 
				
			||||||
 | 
						*f = append(*f, &MountTmpfs{dest, size, perm})
 | 
				
			||||||
 | 
						return f
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() { gob.Register(new(Symlink)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Symlink creates a symlink in the container filesystem.
 | 
				
			||||||
 | 
					type Symlink [2]string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (l *Symlink) early(*Params) error {
 | 
				
			||||||
 | 
						if strings.HasPrefix(l[0], "*") {
 | 
				
			||||||
 | 
							l[0] = l[0][1:]
 | 
				
			||||||
 | 
							if !path.IsAbs(l[0]) {
 | 
				
			||||||
 | 
								return msg.WrapErr(syscall.EBADE,
 | 
				
			||||||
 | 
									fmt.Sprintf("path %q is not absolute", l[0]))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if name, err := os.Readlink(l[0]); err != nil {
 | 
				
			||||||
 | 
								return wrapErrSelf(err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								l[0] = name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *Symlink) apply(params *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 := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := os.Symlink(l[0], target); err != nil {
 | 
				
			||||||
 | 
							return wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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 wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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 *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 wrapErrSelf(err)
 | 
				
			||||||
 | 
						} 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, params.ParentPerm); 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 wrapErrSelf(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										75
									
								
								sandbox/syscall.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								sandbox/syscall.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"unsafe"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						O_PATH = 0x200000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						PR_SET_NO_NEW_PRIVS = 0x26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CAP_SYS_ADMIN = 0x15
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						SUID_DUMP_DISABLE = iota
 | 
				
			||||||
 | 
						SUID_DUMP_USER
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SetDumpable(dumpable uintptr) error {
 | 
				
			||||||
 | 
						// linux/sched/coredump.h
 | 
				
			||||||
 | 
						if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
 | 
				
			||||||
 | 
							return errno
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						_LINUX_CAPABILITY_VERSION_3 = 0x20080522
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						PR_CAP_AMBIENT           = 47
 | 
				
			||||||
 | 
						PR_CAP_AMBIENT_CLEAR_ALL = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CAP_SETPCAP = 8
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type (
 | 
				
			||||||
 | 
						capHeader struct {
 | 
				
			||||||
 | 
							version uint32
 | 
				
			||||||
 | 
							pid     int32
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						capData struct {
 | 
				
			||||||
 | 
							effective   uint32
 | 
				
			||||||
 | 
							permitted   uint32
 | 
				
			||||||
 | 
							inheritable uint32
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func capset(hdrp *capHeader, datap *[2]capData) error {
 | 
				
			||||||
 | 
						if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
 | 
				
			||||||
 | 
							uintptr(unsafe.Pointer(hdrp)),
 | 
				
			||||||
 | 
							uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 {
 | 
				
			||||||
 | 
							return errno
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IgnoringEINTR makes a function call and repeats it if it returns an
 | 
				
			||||||
 | 
					// EINTR error. This appears to be required even though we install all
 | 
				
			||||||
 | 
					// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
 | 
				
			||||||
 | 
					// Also #20400 and #36644 are issues in which a signal handler is
 | 
				
			||||||
 | 
					// installed without setting SA_RESTART. None of these are the common case,
 | 
				
			||||||
 | 
					// but there are enough of them that it seems that we can't avoid
 | 
				
			||||||
 | 
					// an EINTR loop.
 | 
				
			||||||
 | 
					func IgnoringEINTR(fn func() error) error {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							err := fn()
 | 
				
			||||||
 | 
							if err != syscall.EINTR {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								sandbox/sysctl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								sandbox/sysctl.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					package sandbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						kernelOverflowuid int
 | 
				
			||||||
 | 
						kernelOverflowgid int
 | 
				
			||||||
 | 
						kernelCapLastCap  int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sysctlOnce sync.Once
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						kernelOverflowuidPath = "/proc/sys/kernel/overflowuid"
 | 
				
			||||||
 | 
						kernelOverflowgidPath = "/proc/sys/kernel/overflowgid"
 | 
				
			||||||
 | 
						kernelCapLastCapPath  = "/proc/sys/kernel/cap_last_cap"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mustReadSysctl() {
 | 
				
			||||||
 | 
						if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
 | 
				
			||||||
 | 
						} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
 | 
				
			||||||
 | 
						} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
 | 
				
			||||||
 | 
						} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
 | 
				
			||||||
 | 
					func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
 | 
				
			||||||
 | 
					func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }
 | 
				
			||||||
							
								
								
									
										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)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user