Compare commits
	
		
			No commits in common. "7106b0096863e89e1a6809d8c2bed78adf6a3ba5" and "1ec901f79e1a11c26a91893b38c097cfb2b5e05b" have entirely different histories.
		
	
	
		
			7106b00968
			...
			1ec901f79e
		
	
		
@ -38,13 +38,6 @@ type bundleInfo struct {
 | 
			
		||||
	// passed through to [fst.Config]
 | 
			
		||||
	Enablements system.Enablements `json:"enablements"`
 | 
			
		||||
 | 
			
		||||
	// passed through inverted to [bwrap.SyscallPolicy]
 | 
			
		||||
	Devel bool `json:"devel,omitempty"`
 | 
			
		||||
	// passed through to [bwrap.SyscallPolicy]
 | 
			
		||||
	Multiarch bool `json:"multiarch,omitempty"`
 | 
			
		||||
	// passed through to [bwrap.SyscallPolicy]
 | 
			
		||||
	Bluetooth bool `json:"bluetooth,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// allow gpu access within sandbox
 | 
			
		||||
	GPU bool `json:"gpu"`
 | 
			
		||||
	// store path to nixGL mesa wrappers
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -97,7 +96,6 @@ func actionStart(args []string) {
 | 
			
		||||
				UserNS:        app.UserNS,
 | 
			
		||||
				Net:           app.Net,
 | 
			
		||||
				Dev:           app.Dev,
 | 
			
		||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
 | 
			
		||||
				NoNewSession:  app.NoNewSession || dropShell,
 | 
			
		||||
				MapRealUID:    app.MapRealUID,
 | 
			
		||||
				DirectWayland: app.DirectWayland,
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -35,7 +34,6 @@ func withNixDaemon(
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				UserNS:       true, // nix sandbox requires userns
 | 
			
		||||
				Net:          net,
 | 
			
		||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
			
		||||
@ -67,7 +65,6 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
 | 
			
		||||
			Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
			
		||||
			Sandbox: &fst.SandboxConfig{
 | 
			
		||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
			
		||||
				Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
			
		||||
				NoNewSession: dropShell,
 | 
			
		||||
				Filesystem: []*fst.FilesystemConfig{
 | 
			
		||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										69
									
								
								cmd/fuserdb/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								cmd/fuserdb/main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	fmsg.SetPrefix("fuserdb")
 | 
			
		||||
 | 
			
		||||
	const varEmpty = "/var/empty"
 | 
			
		||||
 | 
			
		||||
	out := flag.String("o", "userdb", "output directory")
 | 
			
		||||
	homeDir := flag.String("d", varEmpty, "parent of home directories")
 | 
			
		||||
	shell := flag.String("s", "/sbin/nologin", "absolute path to subordinate user shell")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	type user struct {
 | 
			
		||||
		name string
 | 
			
		||||
		fid  int
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	users := make([]user, len(flag.Args()))
 | 
			
		||||
	for i, s := range flag.Args() {
 | 
			
		||||
		f := bytes.SplitN([]byte(s), []byte{':'}, 2)
 | 
			
		||||
		if len(f) != 2 {
 | 
			
		||||
			fmsg.Fatalf("invalid entry at index %d", i)
 | 
			
		||||
		}
 | 
			
		||||
		users[i].name = string(f[0])
 | 
			
		||||
		if fid, err := strconv.Atoi(string(f[1])); err != nil {
 | 
			
		||||
			fmsg.Fatal(err.Error())
 | 
			
		||||
		} else {
 | 
			
		||||
			users[i].fid = fid
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := os.MkdirAll(*out, 0755); err != nil && !errors.Is(err, os.ErrExist) {
 | 
			
		||||
		fmsg.Fatalf("cannot create output: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range users {
 | 
			
		||||
		fidString := strconv.Itoa(u.fid)
 | 
			
		||||
		for aid := 0; aid < 10000; aid++ {
 | 
			
		||||
			userName := fmt.Sprintf("u%d_a%d", u.fid, aid)
 | 
			
		||||
			uid := 1000000 + u.fid*10000 + aid
 | 
			
		||||
			us := strconv.Itoa(uid)
 | 
			
		||||
			realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name)
 | 
			
		||||
			var homeDirectory string
 | 
			
		||||
			if *homeDir != varEmpty {
 | 
			
		||||
				homeDirectory = path.Join(*homeDir, "u"+fidString, "a"+strconv.Itoa(aid))
 | 
			
		||||
			} else {
 | 
			
		||||
				homeDirectory = varEmpty
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			writeUser(userName, uid, us, realName, homeDirectory, *shell, *out)
 | 
			
		||||
			writeGroup(userName, uid, us, nil, *out)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmsg.Printf("created %d entries", len(users)*2*10000)
 | 
			
		||||
	fmsg.Exit(0)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								cmd/fuserdb/payload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								cmd/fuserdb/payload.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type payloadU struct {
 | 
			
		||||
	UserName      string   `json:"userName"`
 | 
			
		||||
	Uid           int      `json:"uid"`
 | 
			
		||||
	Gid           int      `json:"gid"`
 | 
			
		||||
	MemberOf      []string `json:"memberOf,omitempty"`
 | 
			
		||||
	RealName      string   `json:"realName"`
 | 
			
		||||
	HomeDirectory string   `json:"homeDirectory"`
 | 
			
		||||
	Shell         string   `json:"shell"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeUser(userName string, uid int, us string, realName, homeDirectory, shell string, out string) {
 | 
			
		||||
	userFileName := userName + ".user"
 | 
			
		||||
	if f, err := os.OpenFile(path.Join(out, userFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot create %s: %v", userName, err)
 | 
			
		||||
	} else if err = json.NewEncoder(f).Encode(&payloadU{
 | 
			
		||||
		UserName:      userName,
 | 
			
		||||
		Uid:           uid,
 | 
			
		||||
		Gid:           uid,
 | 
			
		||||
		RealName:      realName,
 | 
			
		||||
		HomeDirectory: homeDirectory,
 | 
			
		||||
		Shell:         shell,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot serialise %s: %v", userName, err)
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		fmsg.Printf("cannot close %s: %v", userName, err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := os.Symlink(userFileName, path.Join(out, us+".user")); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot link %s: %v", userName, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type payloadG struct {
 | 
			
		||||
	GroupName string   `json:"groupName"`
 | 
			
		||||
	Gid       int      `json:"gid"`
 | 
			
		||||
	Members   []string `json:"members,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeGroup(groupName string, gid int, gs string, members []string, out string) {
 | 
			
		||||
	groupFileName := groupName + ".group"
 | 
			
		||||
	if f, err := os.OpenFile(path.Join(out, groupFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot create %s: %v", groupName, err)
 | 
			
		||||
	} else if err = json.NewEncoder(f).Encode(&payloadG{
 | 
			
		||||
		GroupName: groupName,
 | 
			
		||||
		Gid:       gid,
 | 
			
		||||
		Members:   members,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot serialise %s: %v", groupName, err)
 | 
			
		||||
	} else if err = f.Close(); err != nil {
 | 
			
		||||
		fmsg.Printf("cannot close %s: %v", groupName, err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := os.Symlink(groupFileName, path.Join(out, gs+".group")); err != nil {
 | 
			
		||||
		fmsg.Fatalf("cannot link %s: %v", groupName, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -141,7 +141,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
			
		||||
 | 
			
		||||
				t.Run("unsealed start of "+id, func(t *testing.T) {
 | 
			
		||||
					want := "proxy not sealed"
 | 
			
		||||
					if err := p.Start(nil, nil, sandbox, false); err == nil || err.Error() != want {
 | 
			
		||||
					if err := p.Start(nil, nil, sandbox); err == nil || err.Error() != want {
 | 
			
		||||
						t.Errorf("Start() error = %v, wantErr %q",
 | 
			
		||||
							err, errors.New(want))
 | 
			
		||||
						return
 | 
			
		||||
@ -175,7 +175,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				t.Run("sealed start of "+id, func(t *testing.T) {
 | 
			
		||||
					if err := p.Start(nil, output, sandbox, false); err != nil {
 | 
			
		||||
					if err := p.Start(nil, output, sandbox); err != nil {
 | 
			
		||||
						t.Fatalf("Start(nil, nil) error = %v",
 | 
			
		||||
							err)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ func (p *Proxy) String() string {
 | 
			
		||||
	return "(unsealed dbus proxy)"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Proxy) BwrapStatic() []string {
 | 
			
		||||
func (p *Proxy) Bwrap() []string {
 | 
			
		||||
	return p.bwrap.Args()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// Start launches the D-Bus proxy and sets up the Wait method.
 | 
			
		||||
// ready should be buffered and must only be received from once.
 | 
			
		||||
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool) error {
 | 
			
		||||
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
 | 
			
		||||
	p.lock.Lock()
 | 
			
		||||
	defer p.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
@ -67,16 +67,11 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
 | 
			
		||||
			Unshare:       nil,
 | 
			
		||||
			Hostname:      "fortify-dbus",
 | 
			
		||||
			Chdir:         "/",
 | 
			
		||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
			
		||||
			Clearenv:      true,
 | 
			
		||||
			NewSession:    true,
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !seccomp {
 | 
			
		||||
			bc.Syscall = nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// resolve proxy socket directories
 | 
			
		||||
		bindTarget := make(map[string]struct{}, 2)
 | 
			
		||||
		for _, ps := range []string{p.session[1], p.system[1]} {
 | 
			
		||||
@ -115,7 +110,7 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
 | 
			
		||||
			bc.Bind(k, k)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
 | 
			
		||||
		h = helper.MustNewBwrap(bc, p.seal, toolPath, argF)
 | 
			
		||||
		cmd = h.Unwrap()
 | 
			
		||||
		p.bwrap = bc
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,8 @@ cd "$(dirname -- "$0")" || exit 1
 | 
			
		||||
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
 | 
			
		||||
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
 | 
			
		||||
 | 
			
		||||
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
 | 
			
		||||
 | 
			
		||||
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
 | 
			
		||||
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
 | 
			
		||||
    install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
 | 
			
		||||
 | 
			
		||||
@ -132,7 +132,6 @@
 | 
			
		||||
                [
 | 
			
		||||
                  musl
 | 
			
		||||
                  libffi
 | 
			
		||||
                  libseccomp
 | 
			
		||||
                  acl
 | 
			
		||||
                  wayland
 | 
			
		||||
                  wayland-protocols
 | 
			
		||||
@ -173,7 +172,6 @@
 | 
			
		||||
                [
 | 
			
		||||
                  musl
 | 
			
		||||
                  libffi
 | 
			
		||||
                  libseccomp
 | 
			
		||||
                  acl
 | 
			
		||||
                  wayland
 | 
			
		||||
                  wayland-protocols
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,12 @@ package fst
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Tmp = "/.fortify"
 | 
			
		||||
 | 
			
		||||
// Config is used to seal an app
 | 
			
		||||
// Config is used to seal an *App
 | 
			
		||||
type Config struct {
 | 
			
		||||
	// application ID
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
@ -108,10 +107,9 @@ func Template() *Config {
 | 
			
		||||
				Hostname:      "localhost",
 | 
			
		||||
				UserNS:        true,
 | 
			
		||||
				Net:           true,
 | 
			
		||||
				Dev:           true,
 | 
			
		||||
				Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
			
		||||
				NoNewSession:  true,
 | 
			
		||||
				MapRealUID:    true,
 | 
			
		||||
				Dev:           true,
 | 
			
		||||
				DirectWayland: false,
 | 
			
		||||
				// example API credentials pulled from Google Chrome
 | 
			
		||||
				// DO NOT USE THESE IN A REAL BROWSER
 | 
			
		||||
@ -125,8 +123,7 @@ func Template() *Config {
 | 
			
		||||
					{Src: "/run/current-system"},
 | 
			
		||||
					{Src: "/run/opengl-driver"},
 | 
			
		||||
					{Src: "/var/db/nix-channels"},
 | 
			
		||||
					{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
 | 
			
		||||
						Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
			
		||||
					{Src: "/home/chronos", Write: true, Must: true},
 | 
			
		||||
					{Src: "/dev/dri", Device: true},
 | 
			
		||||
				},
 | 
			
		||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
			
		||||
@ -134,10 +131,6 @@ func Template() *Config {
 | 
			
		||||
				AutoEtc:  true,
 | 
			
		||||
				Override: []string{"/var/run/nscd"},
 | 
			
		||||
			},
 | 
			
		||||
			ExtraPerms: []*ExtraPermConfig{
 | 
			
		||||
				{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
 | 
			
		||||
				{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
 | 
			
		||||
			},
 | 
			
		||||
			SystemBus: &dbus.Config{
 | 
			
		||||
				See:       nil,
 | 
			
		||||
				Talk:      []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
			
		||||
 | 
			
		||||
@ -22,8 +22,6 @@ type SandboxConfig struct {
 | 
			
		||||
	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
 | 
			
		||||
@ -52,10 +50,6 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
			
		||||
		return nil, errors.New("nil sandbox config")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.Syscall == nil {
 | 
			
		||||
		fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var uid int
 | 
			
		||||
	if !s.MapRealUID {
 | 
			
		||||
		uid = 65534
 | 
			
		||||
@ -75,7 +69,6 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
			
		||||
		so this capacity should eliminate copies for most setups */
 | 
			
		||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
			
		||||
 | 
			
		||||
		Syscall:       s.Syscall,
 | 
			
		||||
		NewSession:    !s.NoNewSession,
 | 
			
		||||
		DieWithParent: true,
 | 
			
		||||
		AsInit:        true,
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/proc"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BubblewrapName is the file name or path to bubblewrap.
 | 
			
		||||
@ -20,6 +21,8 @@ type bubblewrap struct {
 | 
			
		||||
 | 
			
		||||
	// bwrap pipes
 | 
			
		||||
	control *pipes
 | 
			
		||||
	// sync pipe
 | 
			
		||||
	sync *os.File
 | 
			
		||||
	// returns an array of arguments passed directly
 | 
			
		||||
	// to the child process spawned by bwrap
 | 
			
		||||
	argF func(argsFD, statFD int) []string
 | 
			
		||||
@ -46,6 +49,11 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
			
		||||
		return errors.New("exec: already started")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// pass sync fd to bwrap
 | 
			
		||||
	if b.sync != nil {
 | 
			
		||||
		b.Cmd.Args = append(b.Cmd.Args, "--sync-fd", strconv.Itoa(int(proc.ExtraFile(b.Cmd, b.sync))))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// prepare bwrap pipe and args
 | 
			
		||||
	if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -111,13 +119,8 @@ func (b *bubblewrap) Unwrap() *exec.Cmd {
 | 
			
		||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
			
		||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
			
		||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
			
		||||
func MustNewBwrap(
 | 
			
		||||
	conf *bwrap.Config, name string,
 | 
			
		||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
			
		||||
	extraFiles []*os.File,
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
) Helper {
 | 
			
		||||
	b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
 | 
			
		||||
func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper {
 | 
			
		||||
	b, err := NewBwrap(conf, wt, name, argF)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
@ -128,30 +131,22 @@ func MustNewBwrap(
 | 
			
		||||
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
			
		||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
 | 
			
		||||
// Function argF returns an array of arguments passed directly to the child process.
 | 
			
		||||
func NewBwrap(
 | 
			
		||||
	conf *bwrap.Config, name string,
 | 
			
		||||
	wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
			
		||||
	extraFiles []*os.File,
 | 
			
		||||
	syncFd *os.File,
 | 
			
		||||
) (Helper, error) {
 | 
			
		||||
func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) (Helper, error) {
 | 
			
		||||
	b := new(bubblewrap)
 | 
			
		||||
 | 
			
		||||
	if args, err := NewCheckedArgs(conf.Args()); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		b.control = &pipes{args: args}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.sync = conf.Sync()
 | 
			
		||||
	b.argF = argF
 | 
			
		||||
	b.name = name
 | 
			
		||||
	if wt != nil {
 | 
			
		||||
		b.controlPt = &pipes{args: wt}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Cmd = execCommand(BubblewrapName)
 | 
			
		||||
	b.control = new(pipes)
 | 
			
		||||
	args := conf.Args()
 | 
			
		||||
	if fdArgs, err := conf.FDArgs(syncFd, &extraFiles); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if b.control.args, err = NewCheckedArgs(append(args, fdArgs...)); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		b.Cmd.ExtraFiles = extraFiles
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,6 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/proc"
 | 
			
		||||
)
 | 
			
		||||
import "encoding/gob"
 | 
			
		||||
 | 
			
		||||
type Builder interface {
 | 
			
		||||
	Len() int
 | 
			
		||||
@ -19,11 +12,6 @@ type FSBuilder interface {
 | 
			
		||||
	Builder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FDBuilder interface {
 | 
			
		||||
	Len() int
 | 
			
		||||
	Append(args *[]string, extraFiles *[]*os.File) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(new(pairF))
 | 
			
		||||
	gob.Register(new(stringF))
 | 
			
		||||
@ -57,33 +45,6 @@ func (s stringF) Append(args *[]string) {
 | 
			
		||||
	*args = append(*args, s[0], s[1])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type fileF struct {
 | 
			
		||||
	name string
 | 
			
		||||
	file *os.File
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileF) Len() int {
 | 
			
		||||
	if f.file == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileF) Append(args *[]string, extraFiles *[]*os.File) error {
 | 
			
		||||
	if f.file == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	extraFile(args, extraFiles, f.name, f.file)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extraFile(args *[]string, extraFiles *[]*os.File, name string, f *os.File) {
 | 
			
		||||
	if f == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	*args = append(*args, name, strconv.Itoa(int(proc.ExtraFileSlice(extraFiles, f))))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Args returns a slice of bwrap args corresponding to c.
 | 
			
		||||
func (c *Config) Args() (args []string) {
 | 
			
		||||
	builders := []Builder{
 | 
			
		||||
@ -114,25 +75,3 @@ func (c *Config) Args() (args []string) {
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Config) FDArgs(syncFd *os.File, extraFiles *[]*os.File) (args []string, err error) {
 | 
			
		||||
	builders := []FDBuilder{
 | 
			
		||||
		&seccompBuilder{c},
 | 
			
		||||
		&fileF{positionalArgs[SyncFd], syncFd},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	argc := 0
 | 
			
		||||
	for _, b := range builders {
 | 
			
		||||
		argc += b.Len()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args = make([]string, 0, argc)
 | 
			
		||||
	*extraFiles = slices.Grow(*extraFiles, len(builders))
 | 
			
		||||
 | 
			
		||||
	for _, b := range builders {
 | 
			
		||||
		if err = b.Append(&args, extraFiles); err != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -161,3 +161,10 @@ func (c *Config) SetGID(gid int) *Config {
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSync sets the sync pipe kept open while sandbox is running
 | 
			
		||||
// (--sync-fd FD)
 | 
			
		||||
func (c *Config) SetSync(s *os.File) *Config {
 | 
			
		||||
	c.sync = s
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,9 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	// unshare every namespace we support by default if nil
 | 
			
		||||
	// (--unshare-all)
 | 
			
		||||
@ -47,10 +51,6 @@ type Config struct {
 | 
			
		||||
	// (--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"`
 | 
			
		||||
@ -61,6 +61,10 @@ type Config struct {
 | 
			
		||||
	// (--as-pid-1)
 | 
			
		||||
	AsInit bool `json:"as_init"`
 | 
			
		||||
 | 
			
		||||
	// keep this fd open while sandbox is running
 | 
			
		||||
	// (--sync-fd FD)
 | 
			
		||||
	sync *os.File
 | 
			
		||||
 | 
			
		||||
	/* 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
 | 
			
		||||
@ -74,6 +78,7 @@ type Config struct {
 | 
			
		||||
	    --file FD DEST               Copy from FD to destination DEST
 | 
			
		||||
	    --bind-data FD DEST          Copy from FD to file which is bind-mounted on DEST
 | 
			
		||||
	    --ro-bind-data FD DEST       Copy from FD to file which is readonly bind-mounted on DEST
 | 
			
		||||
	    --seccomp FD                 Load and use seccomp rules from FD (not repeatable)
 | 
			
		||||
	    --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
 | 
			
		||||
@ -85,6 +90,12 @@ type Config struct {
 | 
			
		||||
	among which --args is used internally for passing arguments */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sync keep this fd open while sandbox is running
 | 
			
		||||
// (--sync-fd FD)
 | 
			
		||||
func (c *Config) Sync() *os.File {
 | 
			
		||||
	return c.sync
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UnshareConfig struct {
 | 
			
		||||
	// (--unshare-user)
 | 
			
		||||
	// create new user namespace
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,8 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
			name: "uid gid sync",
 | 
			
		||||
			conf: (new(bwrap.Config)).
 | 
			
		||||
				SetUID(1971).
 | 
			
		||||
				SetGID(100),
 | 
			
		||||
				SetGID(100).
 | 
			
		||||
				SetSync(os.Stdin),
 | 
			
		||||
			want: []string{
 | 
			
		||||
				"--unshare-all", "--unshare-user",
 | 
			
		||||
				"--disable-userns", "--assert-userns-disabled",
 | 
			
		||||
@ -134,6 +135,8 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
				"--uid", "1971",
 | 
			
		||||
				// SetGID(100)
 | 
			
		||||
				"--gid", "100",
 | 
			
		||||
				// SetSync(os.Stdin)
 | 
			
		||||
				// this is set when the process is created
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
@ -243,4 +246,10 @@ func TestConfig_Args(t *testing.T) {
 | 
			
		||||
		}()
 | 
			
		||||
		(new(bwrap.Config)).Persist("/run", "", "")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("sync file", func(t *testing.T) {
 | 
			
		||||
		if s := (new(bwrap.Config)).SetSync(os.Stdout).Sync(); s != os.Stdout {
 | 
			
		||||
			t.Errorf("Sync() = %v", s)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,254 +0,0 @@
 | 
			
		||||
#ifndef _GNU_SOURCE
 | 
			
		||||
#define _GNU_SOURCE // CLONE_NEWUSER
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "seccomp-export.h"
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <assert.h>
 | 
			
		||||
#include <errno.h>
 | 
			
		||||
#include <sys/syscall.h>
 | 
			
		||||
#include <sys/socket.h>
 | 
			
		||||
#include <sys/ioctl.h>
 | 
			
		||||
#include <sys/personality.h>
 | 
			
		||||
#include <sched.h>
 | 
			
		||||
 | 
			
		||||
#if (SCMP_VER_MAJOR < 2) || \
 | 
			
		||||
    (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
 | 
			
		||||
    (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
 | 
			
		||||
#error This package requires libseccomp >= v2.5.1
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
struct f_syscall_act {
 | 
			
		||||
  int                  syscall;
 | 
			
		||||
  int                  m_errno;
 | 
			
		||||
  struct scmp_arg_cmp *arg;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
 | 
			
		||||
 | 
			
		||||
#define SECCOMP_RULESET_ADD(ruleset) do {                                                                      \
 | 
			
		||||
  F_println("adding seccomp ruleset \"" #ruleset "\""); \
 | 
			
		||||
  for (int i = 0; i < LEN(ruleset); i++) {                                                                     \
 | 
			
		||||
    assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS);                                       \
 | 
			
		||||
                                                                                                               \
 | 
			
		||||
    if (ruleset[i].arg)                                                                                        \
 | 
			
		||||
      ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
 | 
			
		||||
    else                                                                                                       \
 | 
			
		||||
      ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0);                  \
 | 
			
		||||
                                                                                                               \
 | 
			
		||||
    if (ret == -EFAULT) {                                                                                      \
 | 
			
		||||
      res = 4;                                                                                                 \
 | 
			
		||||
      goto out;                                                                                                \
 | 
			
		||||
    } else if (ret < 0) {                                                                                      \
 | 
			
		||||
      res = 5;                                                                                                 \
 | 
			
		||||
      errno = -ret;                                                                                            \
 | 
			
		||||
      goto out;                                                                                                \
 | 
			
		||||
    }                                                                                                          \
 | 
			
		||||
  }                                                                                                            \
 | 
			
		||||
} while (0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
int f_tmpfile_fd() {
 | 
			
		||||
  FILE *f = tmpfile();
 | 
			
		||||
  if (f == NULL)
 | 
			
		||||
    return -1;
 | 
			
		||||
  return fileno(f);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
 | 
			
		||||
  int32_t res = 0; // refer to resErr for meaning
 | 
			
		||||
  int allow_multiarch = opts & F_MULTIARCH;
 | 
			
		||||
  int allowed_personality = PER_LINUX;
 | 
			
		||||
 | 
			
		||||
  if (opts & F_LINUX32)
 | 
			
		||||
    allowed_personality = PER_LINUX32;
 | 
			
		||||
 | 
			
		||||
  // flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532
 | 
			
		||||
 | 
			
		||||
  struct f_syscall_act deny_common[] = {
 | 
			
		||||
    // Block dmesg
 | 
			
		||||
    {SCMP_SYS(syslog), EPERM},
 | 
			
		||||
    // Useless old syscall
 | 
			
		||||
    {SCMP_SYS(uselib), EPERM},
 | 
			
		||||
    // Don't allow disabling accounting
 | 
			
		||||
    {SCMP_SYS(acct), EPERM},
 | 
			
		||||
    // Don't allow reading current quota use
 | 
			
		||||
    {SCMP_SYS(quotactl), EPERM},
 | 
			
		||||
 | 
			
		||||
    // Don't allow access to the kernel keyring
 | 
			
		||||
    {SCMP_SYS(add_key), EPERM},
 | 
			
		||||
    {SCMP_SYS(keyctl), EPERM},
 | 
			
		||||
    {SCMP_SYS(request_key), EPERM},
 | 
			
		||||
 | 
			
		||||
    // Scary VM/NUMA ops
 | 
			
		||||
    {SCMP_SYS(move_pages), EPERM},
 | 
			
		||||
    {SCMP_SYS(mbind), EPERM},
 | 
			
		||||
    {SCMP_SYS(get_mempolicy), EPERM},
 | 
			
		||||
    {SCMP_SYS(set_mempolicy), EPERM},
 | 
			
		||||
    {SCMP_SYS(migrate_pages), EPERM},
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct f_syscall_act deny_ns[] = {
 | 
			
		||||
    // Don't allow subnamespace setups:
 | 
			
		||||
    {SCMP_SYS(unshare), EPERM},
 | 
			
		||||
    {SCMP_SYS(setns), EPERM},
 | 
			
		||||
    {SCMP_SYS(mount), EPERM},
 | 
			
		||||
    {SCMP_SYS(umount), EPERM},
 | 
			
		||||
    {SCMP_SYS(umount2), EPERM},
 | 
			
		||||
    {SCMP_SYS(pivot_root), EPERM},
 | 
			
		||||
    {SCMP_SYS(chroot), EPERM},
 | 
			
		||||
#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__)
 | 
			
		||||
    // Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
 | 
			
		||||
    // and flags arguments are reversed so the flags come second
 | 
			
		||||
    {SCMP_SYS(clone), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
 | 
			
		||||
#else
 | 
			
		||||
    // Normally the flags come first
 | 
			
		||||
    {SCMP_SYS(clone), EPERM, &SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // seccomp can't look into clone3()'s struct clone_args to check whether
 | 
			
		||||
    // the flags are OK, so we have no choice but to block clone3().
 | 
			
		||||
    // Return ENOSYS so user-space will fall back to clone().
 | 
			
		||||
    // (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
 | 
			
		||||
    {SCMP_SYS(clone3), ENOSYS},
 | 
			
		||||
 | 
			
		||||
    // New mount manipulation APIs can also change our VFS. There's no
 | 
			
		||||
    // legitimate reason to do these in the sandbox, so block all of them
 | 
			
		||||
    // rather than thinking about which ones might be dangerous.
 | 
			
		||||
    // (CVE-2021-41133)
 | 
			
		||||
    {SCMP_SYS(open_tree), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(move_mount), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(fsopen), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(fsconfig), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(fsmount), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(fspick), ENOSYS},
 | 
			
		||||
    {SCMP_SYS(mount_setattr), ENOSYS},
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct f_syscall_act deny_tty[] = {
 | 
			
		||||
    // Don't allow faking input to the controlling tty (CVE-2017-5226)
 | 
			
		||||
    {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)},
 | 
			
		||||
    // In the unlikely event that the controlling tty is a Linux virtual
 | 
			
		||||
    // console (/dev/tty2 or similar), copy/paste operations have an effect
 | 
			
		||||
    // similar to TIOCSTI (CVE-2023-28100)
 | 
			
		||||
    {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCLINUX)},
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct f_syscall_act deny_devel[] = {
 | 
			
		||||
    // Profiling operations; we expect these to be done by tools from outside
 | 
			
		||||
    // the sandbox.  In particular perf has been the source of many CVEs.
 | 
			
		||||
    {SCMP_SYS(perf_event_open), EPERM},
 | 
			
		||||
    // Don't allow you to switch to bsd emulation or whatnot
 | 
			
		||||
    {SCMP_SYS(personality), EPERM, &SCMP_A0(SCMP_CMP_NE, allowed_personality)},
 | 
			
		||||
 | 
			
		||||
    {SCMP_SYS(ptrace), EPERM}
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Blocklist all but unix, inet, inet6 and netlink
 | 
			
		||||
  struct
 | 
			
		||||
  {
 | 
			
		||||
    int            family;
 | 
			
		||||
    f_syscall_opts flags_mask;
 | 
			
		||||
  } socket_family_allowlist[] = {
 | 
			
		||||
    // NOTE: Keep in numerical order
 | 
			
		||||
    { AF_UNSPEC, 0 },
 | 
			
		||||
    { AF_LOCAL, 0 },
 | 
			
		||||
    { AF_INET, 0 },
 | 
			
		||||
    { AF_INET6, 0 },
 | 
			
		||||
    { AF_NETLINK, 0 },
 | 
			
		||||
    { AF_CAN, F_CAN },
 | 
			
		||||
    { AF_BLUETOOTH, F_BLUETOOTH },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
 | 
			
		||||
  if (ctx == NULL) {
 | 
			
		||||
    res = 1;
 | 
			
		||||
    goto out;
 | 
			
		||||
  } else
 | 
			
		||||
    errno = 0;
 | 
			
		||||
 | 
			
		||||
  int ret;
 | 
			
		||||
 | 
			
		||||
  // We only really need to handle arches on multiarch systems.
 | 
			
		||||
  // If only one arch is supported the default is fine
 | 
			
		||||
  if (arch != 0) {
 | 
			
		||||
    // This *adds* the target arch, instead of replacing the
 | 
			
		||||
    // native one. This is not ideal, because we'd like to only
 | 
			
		||||
    // allow the target arch, but we can't really disallow the
 | 
			
		||||
    // native arch at this point, because then bubblewrap
 | 
			
		||||
    // couldn't continue running.
 | 
			
		||||
    ret = seccomp_arch_add(ctx, arch);
 | 
			
		||||
    if (ret < 0 && ret != -EEXIST) {
 | 
			
		||||
      res = 2;
 | 
			
		||||
      errno = -ret;
 | 
			
		||||
      goto out;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (allow_multiarch && multiarch != 0) {
 | 
			
		||||
      ret = seccomp_arch_add(ctx, multiarch);
 | 
			
		||||
      if (ret < 0 && ret != -EEXIST) {
 | 
			
		||||
        res = 3;
 | 
			
		||||
        errno = -ret;
 | 
			
		||||
        goto out;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SECCOMP_RULESET_ADD(deny_common);
 | 
			
		||||
  if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns);
 | 
			
		||||
  if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty);
 | 
			
		||||
  if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel);
 | 
			
		||||
 | 
			
		||||
  if (!allow_multiarch) {
 | 
			
		||||
    F_println("disabling modify_ldt");
 | 
			
		||||
 | 
			
		||||
    // modify_ldt is a historic source of interesting information leaks,
 | 
			
		||||
    // so it's disabled as a hardening measure.
 | 
			
		||||
    // However, it is required to run old 16-bit applications
 | 
			
		||||
    // as well as some Wine patches, so it's allowed in multiarch.
 | 
			
		||||
    ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(modify_ldt), 0);
 | 
			
		||||
 | 
			
		||||
    // See above for the meaning of EFAULT.
 | 
			
		||||
    if (ret == -EFAULT) {
 | 
			
		||||
      // call fmsg here?
 | 
			
		||||
      res = 4;
 | 
			
		||||
      goto out;
 | 
			
		||||
    } else if (ret < 0) {
 | 
			
		||||
      res = 5;
 | 
			
		||||
      errno = -ret;
 | 
			
		||||
      goto out;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Socket filtering doesn't work on e.g. i386, so ignore failures here
 | 
			
		||||
  // However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
 | 
			
		||||
  // something else: https://github.com/seccomp/libseccomp/issues/8
 | 
			
		||||
  int last_allowed_family = -1;
 | 
			
		||||
  for (int i = 0; i < LEN(socket_family_allowlist); i++) {
 | 
			
		||||
    if (socket_family_allowlist[i].flags_mask != 0 &&
 | 
			
		||||
        (socket_family_allowlist[i].flags_mask & opts) != socket_family_allowlist[i].flags_mask)
 | 
			
		||||
      continue;
 | 
			
		||||
 | 
			
		||||
    for (int disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
 | 
			
		||||
      // Blocklist the in-between valid families
 | 
			
		||||
      seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
 | 
			
		||||
    }
 | 
			
		||||
    last_allowed_family = socket_family_allowlist[i].family;
 | 
			
		||||
  }
 | 
			
		||||
  // 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));
 | 
			
		||||
 | 
			
		||||
  ret = seccomp_export_bpf(ctx, fd);
 | 
			
		||||
  if (ret != 0) {
 | 
			
		||||
    res = 6;
 | 
			
		||||
    errno = -ret;
 | 
			
		||||
    goto out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
out:
 | 
			
		||||
  if (ctx)
 | 
			
		||||
    seccomp_release(ctx);
 | 
			
		||||
 | 
			
		||||
  return res;
 | 
			
		||||
}
 | 
			
		||||
@ -1,22 +0,0 @@
 | 
			
		||||
#include <stdint.h>
 | 
			
		||||
#include <seccomp.h>
 | 
			
		||||
 | 
			
		||||
#if (SCMP_VER_MAJOR < 2) || \
 | 
			
		||||
    (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
 | 
			
		||||
    (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
 | 
			
		||||
#error This package requires libseccomp >= v2.5.1
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
typedef enum {
 | 
			
		||||
  F_DENY_NS    = 1 << 0,
 | 
			
		||||
  F_DENY_TTY   = 1 << 1,
 | 
			
		||||
  F_DENY_DEVEL = 1 << 2,
 | 
			
		||||
  F_MULTIARCH  = 1 << 3,
 | 
			
		||||
  F_LINUX32    = 1 << 4,
 | 
			
		||||
  F_CAN        = 1 << 5,
 | 
			
		||||
  F_BLUETOOTH  = 1 << 6,
 | 
			
		||||
} f_syscall_opts;
 | 
			
		||||
 | 
			
		||||
extern void F_println(char *v);
 | 
			
		||||
int f_tmpfile_fd();
 | 
			
		||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
 | 
			
		||||
@ -1,95 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SyscallPolicy struct {
 | 
			
		||||
	DenyDevel bool `json:"deny_devel"`
 | 
			
		||||
	Multiarch bool `json:"multiarch"`
 | 
			
		||||
	Linux32   bool `json:"linux32"`
 | 
			
		||||
	Can       bool `json:"can"`
 | 
			
		||||
	Bluetooth bool `json:"bluetooth"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type seccompBuilder struct {
 | 
			
		||||
	config *Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *seccompBuilder) Len() int {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *seccompBuilder) Append(args *[]string, extraFiles *[]*os.File) error {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if f, err := s.config.resolveSeccomp(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		extraFile(args, extraFiles, positionalArgs[Seccomp], f)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Config) resolveSeccomp() (*os.File, error) {
 | 
			
		||||
	if c.Syscall == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// resolve seccomp filter opts
 | 
			
		||||
	var (
 | 
			
		||||
		opts    syscallOpts
 | 
			
		||||
		optd    []string
 | 
			
		||||
		optCond = [...]struct {
 | 
			
		||||
			v bool
 | 
			
		||||
			o syscallOpts
 | 
			
		||||
			d string
 | 
			
		||||
		}{
 | 
			
		||||
			{!c.UserNS, flagDenyNS, "denyns"},
 | 
			
		||||
			{c.NewSession, flagDenyTTY, "denytty"},
 | 
			
		||||
			{c.Syscall.DenyDevel, flagDenyDevel, "denydevel"},
 | 
			
		||||
			{c.Syscall.Multiarch, flagMultiarch, "multiarch"},
 | 
			
		||||
			{c.Syscall.Linux32, flagLinux32, "linux32"},
 | 
			
		||||
			{c.Syscall.Can, flagCan, "can"},
 | 
			
		||||
			{c.Syscall.Bluetooth, flagBluetooth, "bluetooth"},
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
	if CPrintln != nil {
 | 
			
		||||
		optd = make([]string, 1, len(optCond)+1)
 | 
			
		||||
		optd[0] = "common"
 | 
			
		||||
	}
 | 
			
		||||
	for _, opt := range optCond {
 | 
			
		||||
		if opt.v {
 | 
			
		||||
			opts |= opt.o
 | 
			
		||||
			if fmsg.Verbose() {
 | 
			
		||||
				optd = append(optd, opt.d)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if CPrintln != nil {
 | 
			
		||||
		CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// export seccomp filter to tmpfile
 | 
			
		||||
	if f, err := tmpfile(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		return f, exportAndSeek(f, opts)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func exportAndSeek(f *os.File, opts syscallOpts) error {
 | 
			
		||||
	if err := exportFilter(f.Fd(), opts); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err := f.Seek(0, io.SeekStart)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
package bwrap
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
#cgo linux pkg-config: --static libseccomp
 | 
			
		||||
 | 
			
		||||
#include "seccomp-export.h"
 | 
			
		||||
*/
 | 
			
		||||
import "C"
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var CPrintln func(v ...any)
 | 
			
		||||
 | 
			
		||||
var resErr = [...]error{
 | 
			
		||||
	0: nil,
 | 
			
		||||
	1: errors.New("seccomp_init failed"),
 | 
			
		||||
	2: errors.New("seccomp_arch_add failed"),
 | 
			
		||||
	3: errors.New("seccomp_arch_add failed (multiarch)"),
 | 
			
		||||
	4: errors.New("internal libseccomp failure"),
 | 
			
		||||
	5: errors.New("seccomp_rule_add failed"),
 | 
			
		||||
	6: errors.New("seccomp_export_bpf failed"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	syscallOpts = C.f_syscall_opts
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	flagDenyNS    syscallOpts = C.F_DENY_NS
 | 
			
		||||
	flagDenyTTY   syscallOpts = C.F_DENY_TTY
 | 
			
		||||
	flagDenyDevel syscallOpts = C.F_DENY_DEVEL
 | 
			
		||||
	flagMultiarch syscallOpts = C.F_MULTIARCH
 | 
			
		||||
	flagLinux32   syscallOpts = C.F_LINUX32
 | 
			
		||||
	flagCan       syscallOpts = C.F_CAN
 | 
			
		||||
	flagBluetooth syscallOpts = C.F_BLUETOOTH
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func tmpfile() (*os.File, error) {
 | 
			
		||||
	fd, err := C.f_tmpfile_fd()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return os.NewFile(uintptr(fd), "tmpfile"), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func exportFilter(fd uintptr, opts syscallOpts) error {
 | 
			
		||||
	var (
 | 
			
		||||
		arch      C.uint32_t = 0
 | 
			
		||||
		multiarch C.uint32_t = 0
 | 
			
		||||
	)
 | 
			
		||||
	switch runtime.GOARCH {
 | 
			
		||||
	case "386":
 | 
			
		||||
		arch = C.SCMP_ARCH_X86
 | 
			
		||||
	case "amd64":
 | 
			
		||||
		arch = C.SCMP_ARCH_X86_64
 | 
			
		||||
		multiarch = C.SCMP_ARCH_X86
 | 
			
		||||
	case "arm":
 | 
			
		||||
		arch = C.SCMP_ARCH_ARM
 | 
			
		||||
	case "arm64":
 | 
			
		||||
		arch = C.SCMP_ARCH_AARCH64
 | 
			
		||||
		multiarch = C.SCMP_ARCH_ARM
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
 | 
			
		||||
	if re := resErr[res]; re != nil {
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return re
 | 
			
		||||
		}
 | 
			
		||||
		return fmt.Errorf("%s: %v", re.Error(), err)
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//export F_println
 | 
			
		||||
func F_println(v *C.char) {
 | 
			
		||||
	if CPrintln != nil {
 | 
			
		||||
		CPrintln(C.GoString(v))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -43,9 +43,6 @@ const (
 | 
			
		||||
	Overlay
 | 
			
		||||
	TmpOverlay
 | 
			
		||||
	ROOverlay
 | 
			
		||||
 | 
			
		||||
	SyncFd
 | 
			
		||||
	Seccomp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var positionalArgs = [...]string{
 | 
			
		||||
@ -73,9 +70,6 @@ var positionalArgs = [...]string{
 | 
			
		||||
	Overlay:    "--overlay",
 | 
			
		||||
	TmpOverlay: "--tmp-overlay",
 | 
			
		||||
	ROOverlay:  "--ro-overlay",
 | 
			
		||||
 | 
			
		||||
	SyncFd:  "--sync-fd",
 | 
			
		||||
	Seccomp: "--seccomp",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PermConfig[T FSBuilder] struct {
 | 
			
		||||
 | 
			
		||||
@ -31,11 +31,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
			helper.BubblewrapName = bubblewrapName
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		h := helper.MustNewBwrap(
 | 
			
		||||
			sc, "fortify",
 | 
			
		||||
			argsWt, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
		h := helper.MustNewBwrap(sc, argsWt, "fortify", argF)
 | 
			
		||||
 | 
			
		||||
		if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
			t.Errorf("Start() error = %v, wantErr %v",
 | 
			
		||||
@ -44,11 +40,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
			
		||||
		if got := helper.MustNewBwrap(
 | 
			
		||||
			sc, "fortify",
 | 
			
		||||
			argsWt, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		); got == nil {
 | 
			
		||||
		if got := helper.MustNewBwrap(sc, argsWt, "fortify", argF); got == nil {
 | 
			
		||||
			t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
 | 
			
		||||
				sc, argsWt, "fortify")
 | 
			
		||||
			return
 | 
			
		||||
@ -64,11 +56,7 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		helper.MustNewBwrap(
 | 
			
		||||
			&bwrap.Config{Hostname: "\x00"}, "fortify",
 | 
			
		||||
			nil, argF,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
		helper.MustNewBwrap(&bwrap.Config{Hostname: "\x00"}, nil, "fortify", argF)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("start notify without pipes panic", func(t *testing.T) {
 | 
			
		||||
@ -81,21 +69,13 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		panic(fmt.Sprintf("unreachable: %v",
 | 
			
		||||
			helper.MustNewBwrap(
 | 
			
		||||
				sc, "fortify",
 | 
			
		||||
				nil, argF,
 | 
			
		||||
				nil, nil,
 | 
			
		||||
			).StartNotify(make(chan error))))
 | 
			
		||||
			helper.MustNewBwrap(sc, nil, "fortify", argF).StartNotify(make(chan error))))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("start without pipes", func(t *testing.T) {
 | 
			
		||||
		helper.InternalReplaceExecCommand(t)
 | 
			
		||||
 | 
			
		||||
		h := helper.MustNewBwrap(
 | 
			
		||||
			sc, "crash-test-dummy",
 | 
			
		||||
			nil, argFChecked,
 | 
			
		||||
			nil, nil,
 | 
			
		||||
		)
 | 
			
		||||
		h := helper.MustNewBwrap(sc, nil, "crash-test-dummy", argFChecked)
 | 
			
		||||
		cmd := h.Unwrap()
 | 
			
		||||
 | 
			
		||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
			
		||||
@ -127,6 +107,6 @@ func TestBwrap(t *testing.T) {
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
			
		||||
		testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
 | 
			
		||||
		testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, argsWt, "crash-test-dummy", argF) })
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ package app
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
			
		||||
@ -29,6 +30,9 @@ type RunState struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type app struct {
 | 
			
		||||
	// single-use config reference
 | 
			
		||||
	ct *appCt
 | 
			
		||||
 | 
			
		||||
	// application unique identifier
 | 
			
		||||
	id *fst.ID
 | 
			
		||||
	// operating system interface
 | 
			
		||||
@ -70,3 +74,24 @@ func New(os linux.System) (App, error) {
 | 
			
		||||
	a.os = os
 | 
			
		||||
	return a, fst.NewAppID(a.id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// appCt ensures its wrapped val is only accessed once
 | 
			
		||||
type appCt struct {
 | 
			
		||||
	val  *fst.Config
 | 
			
		||||
	done *atomic.Bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *appCt) Unwrap() *fst.Config {
 | 
			
		||||
	if !a.done.Load() {
 | 
			
		||||
		defer a.done.Store(true)
 | 
			
		||||
		return a.val
 | 
			
		||||
	}
 | 
			
		||||
	panic("attempted to access config reference twice")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newAppCt(config *fst.Config) (ct *appCt) {
 | 
			
		||||
	ct = new(appCt)
 | 
			
		||||
	ct.done = new(atomic.Bool)
 | 
			
		||||
	ct.val = config
 | 
			
		||||
	return ct
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,6 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			Net:      true,
 | 
			
		||||
			UserNS:   true,
 | 
			
		||||
			Clearenv: true,
 | 
			
		||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
			
		||||
			Chdir:    "/home/chronos",
 | 
			
		||||
			SetEnv: map[string]string{
 | 
			
		||||
				"HOME":              "/home/chronos",
 | 
			
		||||
@ -259,7 +258,6 @@ var testCasesPd = []sealTestCase{
 | 
			
		||||
			UserNS:   true,
 | 
			
		||||
			Chdir:    "/home/chronos",
 | 
			
		||||
			Clearenv: true,
 | 
			
		||||
			Syscall:  new(bwrap.SyscallPolicy),
 | 
			
		||||
			SetEnv: map[string]string{
 | 
			
		||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
			
		||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,8 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path"
 | 
			
		||||
	"regexp"
 | 
			
		||||
@ -14,7 +11,6 @@ import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
			
		||||
@ -51,8 +47,6 @@ type appSeal struct {
 | 
			
		||||
 | 
			
		||||
	// pass-through enablement tracking from config
 | 
			
		||||
	et system.Enablements
 | 
			
		||||
	// initial config gob encoding buffer
 | 
			
		||||
	ct io.WriterTo
 | 
			
		||||
	// wayland socket direct access
 | 
			
		||||
	directWayland bool
 | 
			
		||||
	// extra UpdatePerm ops
 | 
			
		||||
@ -91,14 +85,6 @@ func (a *app) Seal(config *fst.Config) error {
 | 
			
		||||
	// create seal
 | 
			
		||||
	seal := new(appSeal)
 | 
			
		||||
 | 
			
		||||
	// encode initial configuration for state tracking
 | 
			
		||||
	ct := new(bytes.Buffer)
 | 
			
		||||
	if err := gob.NewEncoder(ct).Encode(config); err != nil {
 | 
			
		||||
		return fmsg.WrapErrorSuffix(err,
 | 
			
		||||
			"cannot encode initial config:")
 | 
			
		||||
	}
 | 
			
		||||
	seal.ct = ct
 | 
			
		||||
 | 
			
		||||
	// fetch system constants
 | 
			
		||||
	seal.Paths = a.os.Paths()
 | 
			
		||||
 | 
			
		||||
@ -195,7 +181,6 @@ func (a *app) Seal(config *fst.Config) error {
 | 
			
		||||
		conf := &fst.SandboxConfig{
 | 
			
		||||
			UserNS:       true,
 | 
			
		||||
			Net:          true,
 | 
			
		||||
			Syscall:      new(bwrap.SyscallPolicy),
 | 
			
		||||
			NoNewSession: true,
 | 
			
		||||
			AutoEtc:      true,
 | 
			
		||||
		}
 | 
			
		||||
@ -267,5 +252,6 @@ func (a *app) Seal(config *fst.Config) error {
 | 
			
		||||
 | 
			
		||||
	// seal app and release lock
 | 
			
		||||
	a.seal = seal
 | 
			
		||||
	a.ct = newAppCt(config)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
@ -241,7 +240,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
 | 
			
		||||
		// publish current user's pulse cookie for target user
 | 
			
		||||
		if src, err := discoverPulseCookie(os); err != nil {
 | 
			
		||||
			// not fatal
 | 
			
		||||
			fmsg.VPrintln(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
			
		||||
			fmsg.VPrintln(err.(*fmsg.BaseError).Message())
 | 
			
		||||
		} else {
 | 
			
		||||
			dst := path.Join(seal.share, "pulse-cookie")
 | 
			
		||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
			
		||||
 | 
			
		||||
@ -45,20 +45,33 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// construct shim manager
 | 
			
		||||
	a.shim = shim.New(
 | 
			
		||||
		uint32(a.seal.sys.UID()),
 | 
			
		||||
		a.seal.sys.user.as,
 | 
			
		||||
		a.seal.sys.user.supp,
 | 
			
		||||
		&shim.Payload{
 | 
			
		||||
			Argv:  a.seal.command,
 | 
			
		||||
			Exec:  shimExec,
 | 
			
		||||
			Bwrap: a.seal.sys.bwrap,
 | 
			
		||||
			Home:  a.seal.sys.user.data,
 | 
			
		||||
 | 
			
		||||
			Verbose: fmsg.Verbose(),
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// startup will go ahead, commit system setup
 | 
			
		||||
	if err := a.seal.sys.Commit(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	a.seal.sys.needRevert = true
 | 
			
		||||
 | 
			
		||||
	// export sync pipe from sys
 | 
			
		||||
	a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
 | 
			
		||||
 | 
			
		||||
	// start shim via manager
 | 
			
		||||
	a.shim = new(shim.Shim)
 | 
			
		||||
	waitErr := make(chan error, 1)
 | 
			
		||||
	if startTime, err := a.shim.Start(
 | 
			
		||||
		a.seal.sys.user.as,
 | 
			
		||||
		a.seal.sys.user.supp,
 | 
			
		||||
		a.seal.sys.Sync(),
 | 
			
		||||
	); err != nil {
 | 
			
		||||
	if startTime, err := a.shim.Start(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		// shim process created
 | 
			
		||||
@ -75,28 +88,22 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		// send payload
 | 
			
		||||
		if err = a.shim.Serve(shimSetupCtx, &shim.Payload{
 | 
			
		||||
			Argv:  a.seal.command,
 | 
			
		||||
			Exec:  shimExec,
 | 
			
		||||
			Bwrap: a.seal.sys.bwrap,
 | 
			
		||||
			Home:  a.seal.sys.user.data,
 | 
			
		||||
 | 
			
		||||
			Verbose: fmsg.Verbose(),
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
		if err = a.shim.Serve(shimSetupCtx); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// shim accepted setup payload, create process state
 | 
			
		||||
		sd := state.State{
 | 
			
		||||
			ID:   *a.id,
 | 
			
		||||
			PID:  a.shim.Unwrap().Process.Pid,
 | 
			
		||||
			Time: *startTime,
 | 
			
		||||
			ID:     *a.id,
 | 
			
		||||
			PID:    a.shim.Unwrap().Process.Pid,
 | 
			
		||||
			Config: a.ct.Unwrap(),
 | 
			
		||||
			Time:   *startTime,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// register process state
 | 
			
		||||
		var err0 = new(StateStoreError)
 | 
			
		||||
		err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
 | 
			
		||||
			err0.InnerErr = c.Save(&sd, a.seal.ct)
 | 
			
		||||
			err0.InnerErr = c.Save(&sd)
 | 
			
		||||
		})
 | 
			
		||||
		a.seal.sys.saveState = true
 | 
			
		||||
		if err = err0.equiv("cannot save process state:"); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -6,12 +6,8 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
 | 
			
		||||
	return ExtraFileSlice(&cmd.ExtraFiles, f)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
 | 
			
		||||
	// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
 | 
			
		||||
	fd = uintptr(3 + len(*extraFiles))
 | 
			
		||||
	*extraFiles = append(*extraFiles, f)
 | 
			
		||||
	fd = uintptr(3 + len(cmd.ExtraFiles))
 | 
			
		||||
	cmd.ExtraFiles = append(cmd.ExtraFiles, f)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,10 +5,10 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/proc"
 | 
			
		||||
@ -29,6 +29,14 @@ func Main() {
 | 
			
		||||
		panic("unreachable")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// re-exec
 | 
			
		||||
	if len(os.Args) > 0 && (os.Args[0] != "fortify" || os.Args[1] != "shim" || len(os.Args) != 2) && path.IsAbs(os.Args[0]) {
 | 
			
		||||
		if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil {
 | 
			
		||||
			fmsg.Println("cannot re-exec self:", err)
 | 
			
		||||
			// continue anyway
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// receive setup payload
 | 
			
		||||
	var (
 | 
			
		||||
		payload    Payload
 | 
			
		||||
@ -54,9 +62,8 @@ func Main() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// restore bwrap sync fd
 | 
			
		||||
	var syncFd *os.File
 | 
			
		||||
	if payload.Sync != nil {
 | 
			
		||||
		syncFd = os.NewFile(*payload.Sync, "sync")
 | 
			
		||||
		payload.Bwrap.SetSync(os.NewFile(*payload.Sync, "sync"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// close setup socket
 | 
			
		||||
@ -127,19 +134,17 @@ func Main() {
 | 
			
		||||
	conf.Symlink("fortify", innerInit)
 | 
			
		||||
 | 
			
		||||
	helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
			
		||||
	if fmsg.Verbose() {
 | 
			
		||||
		bwrap.CPrintln = fmsg.Println
 | 
			
		||||
	}
 | 
			
		||||
	if b, err := helper.NewBwrap(
 | 
			
		||||
		conf, innerInit,
 | 
			
		||||
		nil, func(int, int) []string { return make([]string, 0) },
 | 
			
		||||
		extraFiles,
 | 
			
		||||
		syncFd,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
	if b, err := helper.NewBwrap(conf, nil, innerInit,
 | 
			
		||||
		func(int, int) []string { return make([]string, 0) }); err != nil {
 | 
			
		||||
		fmsg.Fatalf("malformed sandbox config: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd := b.Unwrap()
 | 
			
		||||
		cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
			
		||||
		cmd.ExtraFiles = extraFiles
 | 
			
		||||
 | 
			
		||||
		if fmsg.Verbose() {
 | 
			
		||||
			fmsg.VPrintln("bwrap args:", conf.Args())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// run and pass through exit code
 | 
			
		||||
		if err = b.Start(); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -20,12 +20,22 @@ import (
 | 
			
		||||
type Shim struct {
 | 
			
		||||
	// user switcher process
 | 
			
		||||
	cmd *exec.Cmd
 | 
			
		||||
	// uid of shim target user
 | 
			
		||||
	uid uint32
 | 
			
		||||
	// string representation of application id
 | 
			
		||||
	aid string
 | 
			
		||||
	// string representation of supplementary group ids
 | 
			
		||||
	supp []string
 | 
			
		||||
	// fallback exit notifier with error returned killing the process
 | 
			
		||||
	killFallback chan error
 | 
			
		||||
	// shim setup payload
 | 
			
		||||
	payload *Payload
 | 
			
		||||
	// monitor to shim encoder
 | 
			
		||||
	encoder *gob.Encoder
 | 
			
		||||
	// bwrap --sync-fd value
 | 
			
		||||
	sync *uintptr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(uid uint32, aid string, supp []string, payload *Payload) *Shim {
 | 
			
		||||
	return &Shim{uid: uid, aid: aid, supp: supp, payload: payload}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) String() string {
 | 
			
		||||
@ -43,14 +53,7 @@ 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) {
 | 
			
		||||
func (s *Shim) Start() (*time.Time, error) {
 | 
			
		||||
	// prepare user switcher invocation
 | 
			
		||||
	var fsu string
 | 
			
		||||
	if p, ok := internal.Path(internal.Fsu); !ok {
 | 
			
		||||
@ -69,22 +72,22 @@ func (s *Shim) Start(
 | 
			
		||||
		s.encoder = e
 | 
			
		||||
		s.cmd.Env = []string{
 | 
			
		||||
			Env + "=" + strconv.Itoa(fd),
 | 
			
		||||
			"FORTIFY_APP_ID=" + aid,
 | 
			
		||||
			"FORTIFY_APP_ID=" + s.aid,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// format fsu supplementary groups
 | 
			
		||||
	if len(supp) > 0 {
 | 
			
		||||
		fmsg.VPrintf("attaching supplementary group ids %s", supp)
 | 
			
		||||
		s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
			
		||||
	if len(s.supp) > 0 {
 | 
			
		||||
		fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
 | 
			
		||||
		s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.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
 | 
			
		||||
	if s.payload.Bwrap.Sync() != nil {
 | 
			
		||||
		fd := proc.ExtraFile(s.cmd, s.payload.Bwrap.Sync())
 | 
			
		||||
		s.payload.Sync = &fd
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmsg.VPrintln("starting shim via fsu:", s.cmd)
 | 
			
		||||
@ -98,7 +101,7 @@ func (s *Shim) Start(
 | 
			
		||||
	return &startTime, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
			
		||||
func (s *Shim) Serve(ctx context.Context) error {
 | 
			
		||||
	// kill shim if something goes wrong and an error is returned
 | 
			
		||||
	s.killFallback = make(chan error, 1)
 | 
			
		||||
	killShim := func() {
 | 
			
		||||
@ -108,9 +111,8 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
			
		||||
	}
 | 
			
		||||
	defer func() { killShim() }()
 | 
			
		||||
 | 
			
		||||
	payload.Sync = s.sync
 | 
			
		||||
	encodeErr := make(chan error)
 | 
			
		||||
	go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
			
		||||
	go func() { encodeErr <- s.encoder.Encode(s.payload) }()
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	// encode return indicates setup completion
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,6 @@
 | 
			
		||||
package shim
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
)
 | 
			
		||||
import "git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
 | 
			
		||||
const Env = "FORTIFY_SHIM"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,9 @@
 | 
			
		||||
package state
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
@ -210,11 +208,12 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
 | 
			
		||||
				s := new(State)
 | 
			
		||||
				r[*id] = s
 | 
			
		||||
 | 
			
		||||
				// append regardless, but only parse if required, implements Len
 | 
			
		||||
				// append regardless, but only parse if required, used to implement Len
 | 
			
		||||
				if decode {
 | 
			
		||||
					if err = b.decodeState(f, s); err != nil {
 | 
			
		||||
					if err = gob.NewDecoder(f).Decode(s); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if s.ID != *id {
 | 
			
		||||
						return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID)
 | 
			
		||||
					}
 | 
			
		||||
@ -230,65 +229,18 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// state file consists of an eight byte header, followed by concatenated gobs
 | 
			
		||||
// of [fst.Config] and [State], if [State.Config] is not nil or offset < 0,
 | 
			
		||||
// the first gob is skipped
 | 
			
		||||
func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
 | 
			
		||||
	offset := make([]byte, 8)
 | 
			
		||||
	if l, err := r.Read(offset); err != nil {
 | 
			
		||||
		if errors.Is(err, io.EOF) {
 | 
			
		||||
			return fmt.Errorf("state file too short: %d bytes", l)
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// decode volatile state first
 | 
			
		||||
	var skipConfig bool
 | 
			
		||||
	{
 | 
			
		||||
		o := int64(binary.LittleEndian.Uint64(offset))
 | 
			
		||||
		skipConfig = o < 0
 | 
			
		||||
 | 
			
		||||
		if !skipConfig {
 | 
			
		||||
			if l, err := r.Seek(o, io.SeekCurrent); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			} else if l != 8+o {
 | 
			
		||||
				return fmt.Errorf("invalid seek offset %d", l)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if err := gob.NewDecoder(r).Decode(state); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// decode sealed config
 | 
			
		||||
	if state.Config == nil {
 | 
			
		||||
		// config must be provided either as part of volatile state,
 | 
			
		||||
		// or in the config segment
 | 
			
		||||
		if skipConfig {
 | 
			
		||||
			return ErrNoConfig
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		state.Config = new(fst.Config)
 | 
			
		||||
		if _, err := r.Seek(8, io.SeekStart); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return gob.NewDecoder(r).Decode(state.Config)
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save writes process state to filesystem
 | 
			
		||||
func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error {
 | 
			
		||||
func (b *multiBackend) Save(state *State) error {
 | 
			
		||||
	b.lock.Lock()
 | 
			
		||||
	defer b.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	if configWriter == nil && state.Config == nil {
 | 
			
		||||
		return ErrNoConfig
 | 
			
		||||
	if state.Config == nil {
 | 
			
		||||
		return errors.New("state does not contain config")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	statePath := b.filename(&state.ID)
 | 
			
		||||
 | 
			
		||||
	// create and open state data file
 | 
			
		||||
	if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
@ -298,43 +250,11 @@ func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error {
 | 
			
		||||
				panic("state file closed prematurely")
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
		return b.encodeState(f, state, configWriter)
 | 
			
		||||
		// encode into state file
 | 
			
		||||
		return gob.NewEncoder(f).Encode(state)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter io.WriterTo) error {
 | 
			
		||||
	offset := make([]byte, 8)
 | 
			
		||||
 | 
			
		||||
	// skip header bytes
 | 
			
		||||
	if _, err := w.Seek(8, io.SeekStart); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if configWriter != nil {
 | 
			
		||||
		// write config gob and encode header
 | 
			
		||||
		if l, err := configWriter.WriteTo(w); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			binary.LittleEndian.PutUint64(offset, uint64(l))
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// offset == -1 indicates absence of config gob
 | 
			
		||||
		binary.LittleEndian.PutUint64(offset, 0xffffffffffffffff)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// encode volatile state
 | 
			
		||||
	if err := gob.NewEncoder(w).Encode(state); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write header
 | 
			
		||||
	if _, err := w.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err := w.Write(offset)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *multiBackend) Destroy(id fst.ID) error {
 | 
			
		||||
	b.lock.Lock()
 | 
			
		||||
	defer b.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,11 @@
 | 
			
		||||
package state
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrNoConfig = errors.New("state does not contain config")
 | 
			
		||||
 | 
			
		||||
type Entries map[fst.ID]*State
 | 
			
		||||
 | 
			
		||||
type Store interface {
 | 
			
		||||
@ -28,13 +24,13 @@ type Store interface {
 | 
			
		||||
 | 
			
		||||
// Cursor provides access to the store
 | 
			
		||||
type Cursor interface {
 | 
			
		||||
	Save(state *State, configWriter io.WriterTo) error
 | 
			
		||||
	Save(state *State) error
 | 
			
		||||
	Destroy(id fst.ID) error
 | 
			
		||||
	Load() (Entries, error)
 | 
			
		||||
	Len() (int, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// State is a fortify process's state
 | 
			
		||||
// State is the on-disk format for a fortified process's state information
 | 
			
		||||
type State struct {
 | 
			
		||||
	// fortify instance id
 | 
			
		||||
	ID fst.ID `json:"instance"`
 | 
			
		||||
@ -44,5 +40,5 @@ type State struct {
 | 
			
		||||
	Config *fst.Config `json:"config"`
 | 
			
		||||
 | 
			
		||||
	// process start time
 | 
			
		||||
	Time time.Time `json:"time"`
 | 
			
		||||
	Time time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
package state_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"io"
 | 
			
		||||
	"math/rand/v2"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"slices"
 | 
			
		||||
@ -31,12 +28,9 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
		tl
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	var tc [tl]struct {
 | 
			
		||||
		state state.State
 | 
			
		||||
		ct    bytes.Buffer
 | 
			
		||||
	}
 | 
			
		||||
	var tc [tl]state.State
 | 
			
		||||
	for i := 0; i < tl; i++ {
 | 
			
		||||
		makeState(t, &tc[i].state, &tc[i].ct)
 | 
			
		||||
		makeState(t, &tc[i])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	do := func(aid int, f func(c state.Cursor)) {
 | 
			
		||||
@ -47,7 +41,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
 | 
			
		||||
	insert := func(i, aid int) {
 | 
			
		||||
		do(aid, func(c state.Cursor) {
 | 
			
		||||
			if err := c.Save(&tc[i].state, &tc[i].ct); err != nil {
 | 
			
		||||
			if err := c.Save(&tc[i]); err != nil {
 | 
			
		||||
				t.Fatalf("Save(&tc[%v]): error = %v", i, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
@ -57,17 +51,15 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
		do(aid, func(c state.Cursor) {
 | 
			
		||||
			if entries, err := c.Load(); err != nil {
 | 
			
		||||
				t.Fatalf("Load: error = %v", err)
 | 
			
		||||
			} else if got, ok := entries[tc[i].state.ID]; !ok {
 | 
			
		||||
			} else if got, ok := entries[tc[i].ID]; !ok {
 | 
			
		||||
				t.Fatalf("Load: entry %s missing",
 | 
			
		||||
					&tc[i].state.ID)
 | 
			
		||||
					&tc[i].ID)
 | 
			
		||||
			} else {
 | 
			
		||||
				got.Time = tc[i].state.Time
 | 
			
		||||
				tc[i].state.Config = fst.Template()
 | 
			
		||||
				if !reflect.DeepEqual(got, &tc[i].state) {
 | 
			
		||||
				got.Time = tc[i].Time
 | 
			
		||||
				if !reflect.DeepEqual(got, &tc[i]) {
 | 
			
		||||
					t.Fatalf("Load: entry %s got %#v, want %#v",
 | 
			
		||||
						&tc[i].state.ID, got, &tc[i].state)
 | 
			
		||||
						&tc[i].ID, got, &tc[i])
 | 
			
		||||
				}
 | 
			
		||||
				tc[i].state.Config = nil
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
@ -112,7 +104,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
 | 
			
		||||
	t.Run("clear aid 1", func(t *testing.T) {
 | 
			
		||||
		do(1, func(c state.Cursor) {
 | 
			
		||||
			if err := c.Destroy(tc[insertEntryOtherApp].state.ID); err != nil {
 | 
			
		||||
			if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
 | 
			
		||||
				t.Fatalf("Destroy: error = %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
@ -132,13 +124,11 @@ func testStore(t *testing.T, s state.Store) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeState(t *testing.T, s *state.State, ct io.Writer) {
 | 
			
		||||
func makeState(t *testing.T, s *state.State) {
 | 
			
		||||
	if err := fst.NewAppID(&s.ID); err != nil {
 | 
			
		||||
		t.Fatalf("cannot create dummy state: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
 | 
			
		||||
		t.Fatalf("cannot encode dummy config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	s.Config = fst.Template()
 | 
			
		||||
	s.PID = rand.Int()
 | 
			
		||||
	s.Time = time.Now()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -93,13 +93,13 @@ func (d *DBus) apply(_ *I) error {
 | 
			
		||||
	ready := make(chan error, 1)
 | 
			
		||||
 | 
			
		||||
	// background dbus proxy start
 | 
			
		||||
	if err := d.proxy.Start(ready, d.out, true, true); err != nil {
 | 
			
		||||
	if err := d.proxy.Start(ready, d.out, true); err != nil {
 | 
			
		||||
		return fmsg.WrapErrorSuffix(err,
 | 
			
		||||
			"cannot start message bus proxy:")
 | 
			
		||||
	}
 | 
			
		||||
	fmsg.VPrintln("starting message bus proxy:", d.proxy)
 | 
			
		||||
	if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
 | 
			
		||||
		fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.BwrapStatic())
 | 
			
		||||
		fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// background wait for proxy instance and notify completion
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								ldd/exec.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								ldd/exec.go
									
									
									
									
									
								
							@ -16,17 +16,13 @@ func Exec(p string) ([]*Entry, error) {
 | 
			
		||||
		cmd *exec.Cmd
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if b, err := helper.NewBwrap(
 | 
			
		||||
		(&bwrap.Config{
 | 
			
		||||
			Hostname:      "fortify-ldd",
 | 
			
		||||
			Chdir:         "/",
 | 
			
		||||
			Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
			
		||||
			NewSession:    true,
 | 
			
		||||
			DieWithParent: true,
 | 
			
		||||
		}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
 | 
			
		||||
		nil, func(_, _ int) []string { return []string{p} },
 | 
			
		||||
		nil, nil,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
	if b, err := helper.NewBwrap((&bwrap.Config{
 | 
			
		||||
		Hostname:      "fortify-ldd",
 | 
			
		||||
		Chdir:         "/",
 | 
			
		||||
		NewSession:    true,
 | 
			
		||||
		DieWithParent: true,
 | 
			
		||||
	}).Bind("/", "/").DevTmpfs("/dev"),
 | 
			
		||||
		nil, "ldd", func(_, _ int) []string { return []string{p} }); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = b.Unwrap()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								main.go
									
									
									
									
									
								
							@ -16,7 +16,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
			
		||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
			
		||||
@ -309,10 +308,6 @@ func runApp(config *fst.Config) {
 | 
			
		||||
	rs := new(app.RunState)
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
 | 
			
		||||
	if fmsg.Verbose() {
 | 
			
		||||
		bwrap.CPrintln = fmsg.Println
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// handle signals for graceful shutdown
 | 
			
		||||
	sig := make(chan os.Signal, 2)
 | 
			
		||||
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										104
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								nixos.nix
									
									
									
									
									
								
							@ -7,7 +7,6 @@
 | 
			
		||||
 | 
			
		||||
let
 | 
			
		||||
  inherit (lib)
 | 
			
		||||
    mkMerge
 | 
			
		||||
    mkIf
 | 
			
		||||
    mkDefault
 | 
			
		||||
    mapAttrs
 | 
			
		||||
@ -20,10 +19,6 @@ let
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
  cfg = config.environment.fortify;
 | 
			
		||||
 | 
			
		||||
  getsubuid = fid: aid: 1000000 + fid * 10000 + aid;
 | 
			
		||||
  getsubname = fid: aid: "u${toString fid}_a${toString aid}";
 | 
			
		||||
  getsubhome = fid: aid: "${cfg.stateDir}/u${toString fid}/a${toString aid}";
 | 
			
		||||
in
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
@ -38,12 +33,23 @@ in
 | 
			
		||||
      group = "root";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    environment.etc.fsurc = {
 | 
			
		||||
      mode = "0400";
 | 
			
		||||
      text = foldlAttrs (
 | 
			
		||||
        acc: username: fid:
 | 
			
		||||
        "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
 | 
			
		||||
      ) "" cfg.users;
 | 
			
		||||
    environment.etc = {
 | 
			
		||||
      fsurc = {
 | 
			
		||||
        mode = "0400";
 | 
			
		||||
        text = foldlAttrs (
 | 
			
		||||
          acc: username: fid:
 | 
			
		||||
          "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
 | 
			
		||||
        ) "" cfg.users;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      userdb.source = pkgs.runCommand "fortify-userdb" { } ''
 | 
			
		||||
        ${cfg.package}/libexec/fuserdb -o $out ${
 | 
			
		||||
          foldlAttrs (
 | 
			
		||||
            acc: username: fid:
 | 
			
		||||
            acc + " ${username}:${toString fid}"
 | 
			
		||||
          ) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users
 | 
			
		||||
        }
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
 | 
			
		||||
@ -108,8 +114,8 @@ in
 | 
			
		||||
                    confinement = {
 | 
			
		||||
                      app_id = aid;
 | 
			
		||||
                      inherit (app) groups;
 | 
			
		||||
                      username = getsubname fid aid;
 | 
			
		||||
                      home = getsubhome fid aid;
 | 
			
		||||
                      username = "u${toString fid}_a${toString aid}";
 | 
			
		||||
                      home = "${cfg.stateDir}/u${toString fid}/a${toString aid}";
 | 
			
		||||
                      sandbox = {
 | 
			
		||||
                        inherit (app)
 | 
			
		||||
                          userns
 | 
			
		||||
@ -117,9 +123,6 @@ in
 | 
			
		||||
                          dev
 | 
			
		||||
                          env
 | 
			
		||||
                          ;
 | 
			
		||||
                        syscall = {
 | 
			
		||||
                          inherit (app) devel multiarch bluetooth;
 | 
			
		||||
                        };
 | 
			
		||||
                        map_real_uid = app.mapRealUid;
 | 
			
		||||
                        no_new_session = app.tty;
 | 
			
		||||
                        filesystem =
 | 
			
		||||
@ -170,9 +173,7 @@ in
 | 
			
		||||
                  };
 | 
			
		||||
                in
 | 
			
		||||
                pkgs.writeShellScriptBin app.name ''
 | 
			
		||||
                  exec fortify${
 | 
			
		||||
                    if app.verbose then " -v" else ""
 | 
			
		||||
                  } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
			
		||||
                  exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
			
		||||
                ''
 | 
			
		||||
              ) cfg.apps;
 | 
			
		||||
            in
 | 
			
		||||
@ -204,63 +205,16 @@ in
 | 
			
		||||
 | 
			
		||||
        users = foldlAttrs (
 | 
			
		||||
          acc: _: fid:
 | 
			
		||||
          mkMerge [
 | 
			
		||||
            (mergeAttrsList (
 | 
			
		||||
              # aid 0 is reserved
 | 
			
		||||
              imap1 (aid: app: {
 | 
			
		||||
                ${getsubname fid aid} = mkMerge [
 | 
			
		||||
                  (cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
 | 
			
		||||
                  app.extraConfig
 | 
			
		||||
                  { home.packages = app.packages; }
 | 
			
		||||
                ];
 | 
			
		||||
              }) cfg.apps
 | 
			
		||||
            ))
 | 
			
		||||
            { ${getsubname fid 0} = cfg.home-manager (getsubname fid 0) (getsubuid fid 0); }
 | 
			
		||||
            acc
 | 
			
		||||
          ]
 | 
			
		||||
          mergeAttrsList (
 | 
			
		||||
            # aid 0 is reserved
 | 
			
		||||
            imap1 (aid: app: {
 | 
			
		||||
              "u${toString fid}_a${toString aid}" = app.extraConfig // {
 | 
			
		||||
                home.packages = app.packages;
 | 
			
		||||
              };
 | 
			
		||||
            }) cfg.apps
 | 
			
		||||
          )
 | 
			
		||||
          // acc
 | 
			
		||||
        ) privPackages cfg.users;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
    users =
 | 
			
		||||
      let
 | 
			
		||||
        getuser = fid: aid: {
 | 
			
		||||
          isSystemUser = true;
 | 
			
		||||
          createHome = true;
 | 
			
		||||
          description = "Fortify subordinate user ${toString aid} (u${toString fid})";
 | 
			
		||||
          group = getsubname fid aid;
 | 
			
		||||
          home = getsubhome fid aid;
 | 
			
		||||
          uid = getsubuid fid aid;
 | 
			
		||||
        };
 | 
			
		||||
        getgroup = fid: aid: { gid = getsubuid fid aid; };
 | 
			
		||||
      in
 | 
			
		||||
      {
 | 
			
		||||
        users = foldlAttrs (
 | 
			
		||||
          acc: _: fid:
 | 
			
		||||
          mkMerge [
 | 
			
		||||
            (mergeAttrsList (
 | 
			
		||||
              # aid 0 is reserved
 | 
			
		||||
              imap1 (aid: _: {
 | 
			
		||||
                ${getsubname fid aid} = getuser fid aid;
 | 
			
		||||
              }) cfg.apps
 | 
			
		||||
            ))
 | 
			
		||||
            { ${getsubname fid 0} = getuser fid 0; }
 | 
			
		||||
            acc
 | 
			
		||||
          ]
 | 
			
		||||
        ) { } cfg.users;
 | 
			
		||||
 | 
			
		||||
        groups = foldlAttrs (
 | 
			
		||||
          acc: _: fid:
 | 
			
		||||
          mkMerge [
 | 
			
		||||
            (mergeAttrsList (
 | 
			
		||||
              # aid 0 is reserved
 | 
			
		||||
              imap1 (aid: _: {
 | 
			
		||||
                ${getsubname fid aid} = getgroup fid aid;
 | 
			
		||||
              }) cfg.apps
 | 
			
		||||
            ))
 | 
			
		||||
            { ${getsubname fid 0} = getgroup fid 0; }
 | 
			
		||||
            acc
 | 
			
		||||
          ]
 | 
			
		||||
        ) { } cfg.users;
 | 
			
		||||
      };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								options.md
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								options.md
									
									
									
									
									
								
							@ -36,7 +36,7 @@ package
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Default:*
 | 
			
		||||
` <derivation fortify-0.2.11> `
 | 
			
		||||
` <derivation fortify-0.2.10> `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -77,30 +77,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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -242,31 +218,7 @@ null or anything
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable access to all devices\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Type:*
 | 
			
		||||
boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Default:*
 | 
			
		||||
` false `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Example:*
 | 
			
		||||
` true `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## environment\.fortify\.apps\.\*\.devel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable development kernel APIs\.
 | 
			
		||||
Whether to enable access to all devices within the sandbox\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -405,31 +357,7 @@ null or string
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable mapping to priv-user uid\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Type:*
 | 
			
		||||
boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Default:*
 | 
			
		||||
` false `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Example:*
 | 
			
		||||
` true `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## environment\.fortify\.apps\.\*\.multiarch
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable multiarch kernel support\.
 | 
			
		||||
Whether to enable mapping to fortify’s real UID within the sandbox\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -467,7 +395,7 @@ string
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable network access\.
 | 
			
		||||
Whether to enable network access within the sandbox\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -491,7 +419,7 @@ boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable nix daemon\.
 | 
			
		||||
Whether to enable nix daemon access within the sandbox\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -554,7 +482,7 @@ null or package
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable access to the controlling terminal\.
 | 
			
		||||
Whether to enable allow access to the controlling terminal\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -578,7 +506,7 @@ boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable user namespace\.
 | 
			
		||||
Whether to enable userns within the sandbox\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -598,44 +526,6 @@ boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## environment\.fortify\.apps\.\*\.verbose
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Whether to enable launchers with verbose output\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Type:*
 | 
			
		||||
boolean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Default:*
 | 
			
		||||
` false `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Example:*
 | 
			
		||||
` true `
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## environment\.fortify\.home-manager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Target user shared home-manager configuration\.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Type:*
 | 
			
		||||
function that evaluates to a(n) function that evaluates to a(n) attribute set of anything
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## environment\.fortify\.stateDir
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								options.nix
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								options.nix
									
									
									
									
									
								
							@ -26,17 +26,6 @@ in
 | 
			
		||||
        '';
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      home-manager = mkOption {
 | 
			
		||||
        type =
 | 
			
		||||
          let
 | 
			
		||||
            inherit (types) functionTo attrsOf anything;
 | 
			
		||||
          in
 | 
			
		||||
          functionTo (functionTo (attrsOf anything));
 | 
			
		||||
        description = ''
 | 
			
		||||
          Target user shared home-manager configuration.
 | 
			
		||||
        '';
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      apps = mkOption {
 | 
			
		||||
        type =
 | 
			
		||||
          let
 | 
			
		||||
@ -61,8 +50,6 @@ in
 | 
			
		||||
                '';
 | 
			
		||||
              };
 | 
			
		||||
 | 
			
		||||
              verbose = mkEnableOption "launchers with verbose output";
 | 
			
		||||
 | 
			
		||||
              id = mkOption {
 | 
			
		||||
                type = nullOr str;
 | 
			
		||||
                default = null;
 | 
			
		||||
@ -141,20 +128,16 @@ in
 | 
			
		||||
                '';
 | 
			
		||||
              };
 | 
			
		||||
 | 
			
		||||
              nix = mkEnableOption "nix daemon";
 | 
			
		||||
              userns = mkEnableOption "user namespace";
 | 
			
		||||
              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
			
		||||
              dev = mkEnableOption "access to all devices";
 | 
			
		||||
              tty = mkEnableOption "access to the controlling terminal";
 | 
			
		||||
              nix = mkEnableOption "nix daemon access within the sandbox";
 | 
			
		||||
              userns = mkEnableOption "userns within the sandbox";
 | 
			
		||||
              mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
 | 
			
		||||
              dev = mkEnableOption "access to all devices within the sandbox";
 | 
			
		||||
              tty = mkEnableOption "allow access to the controlling terminal";
 | 
			
		||||
 | 
			
		||||
              net = mkEnableOption "network access" // {
 | 
			
		||||
              net = mkEnableOption "network access within the sandbox" // {
 | 
			
		||||
                default = true;
 | 
			
		||||
              };
 | 
			
		||||
 | 
			
		||||
              devel = mkEnableOption "development kernel APIs";
 | 
			
		||||
              multiarch = mkEnableOption "multiarch kernel support";
 | 
			
		||||
              bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
 | 
			
		||||
 | 
			
		||||
              gpu = mkOption {
 | 
			
		||||
                type = nullOr bool;
 | 
			
		||||
                default = null;
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@
 | 
			
		||||
  bubblewrap,
 | 
			
		||||
  pkg-config,
 | 
			
		||||
  libffi,
 | 
			
		||||
  libseccomp,
 | 
			
		||||
  acl,
 | 
			
		||||
  wayland,
 | 
			
		||||
  wayland-protocols,
 | 
			
		||||
@ -16,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
buildGoModule rec {
 | 
			
		||||
  pname = "fortify";
 | 
			
		||||
  version = "0.2.11";
 | 
			
		||||
  version = "0.2.10";
 | 
			
		||||
 | 
			
		||||
  src = builtins.path {
 | 
			
		||||
    name = "fortify-src";
 | 
			
		||||
@ -46,7 +45,6 @@ buildGoModule rec {
 | 
			
		||||
  buildInputs =
 | 
			
		||||
    [
 | 
			
		||||
      libffi
 | 
			
		||||
      libseccomp
 | 
			
		||||
      acl
 | 
			
		||||
      wayland
 | 
			
		||||
      wayland-protocols
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								print.go
									
									
									
									
									
								
							@ -53,10 +53,6 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
 | 
			
		||||
	now := time.Now().UTC()
 | 
			
		||||
	w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
 | 
			
		||||
 | 
			
		||||
	if config.Confinement.Sandbox == nil {
 | 
			
		||||
		fmt.Print("Warning: this configuration uses permissive defaults!\n\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if instance != nil {
 | 
			
		||||
		fmt.Fprintf(w, "State\n")
 | 
			
		||||
		fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
 | 
			
		||||
@ -110,6 +106,9 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
 | 
			
		||||
 | 
			
		||||
		// Env           map[string]string   `json:"env"`
 | 
			
		||||
		// Link          [][2]string         `json:"symlink"`
 | 
			
		||||
	} else {
 | 
			
		||||
		// this gets printed before everything else
 | 
			
		||||
		fmt.Println("WARNING: current configuration uses permissive defaults!")
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
 | 
			
		||||
	fmt.Fprintf(w, "\n")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								test.nix
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								test.nix
									
									
									
									
									
								
							@ -44,6 +44,7 @@ nixosTest {
 | 
			
		||||
          # For glinfo and wayland-info:
 | 
			
		||||
          mesa-demos
 | 
			
		||||
          wayland-utils
 | 
			
		||||
          alacritty
 | 
			
		||||
 | 
			
		||||
          # For D-Bus tests:
 | 
			
		||||
          libnotify
 | 
			
		||||
@ -82,7 +83,7 @@ nixosTest {
 | 
			
		||||
          sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
 | 
			
		||||
 | 
			
		||||
          sway --validate
 | 
			
		||||
          systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
 | 
			
		||||
          sway && touch /tmp/sway-exit-ok
 | 
			
		||||
        fi
 | 
			
		||||
      '';
 | 
			
		||||
 | 
			
		||||
@ -110,43 +111,6 @@ nixosTest {
 | 
			
		||||
        enable = true;
 | 
			
		||||
        stateDir = "/var/lib/fortify";
 | 
			
		||||
        users.alice = 0;
 | 
			
		||||
 | 
			
		||||
        home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
			
		||||
 | 
			
		||||
        apps = [
 | 
			
		||||
          {
 | 
			
		||||
            name = "ne-foot";
 | 
			
		||||
            verbose = true;
 | 
			
		||||
            share = pkgs.foot;
 | 
			
		||||
            packages = [ pkgs.foot ];
 | 
			
		||||
            command = "foot";
 | 
			
		||||
            capability = {
 | 
			
		||||
              dbus = false;
 | 
			
		||||
              pulse = false;
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            name = "pa-foot";
 | 
			
		||||
            verbose = true;
 | 
			
		||||
            share = pkgs.foot;
 | 
			
		||||
            packages = [ pkgs.foot ];
 | 
			
		||||
            command = "foot";
 | 
			
		||||
            capability.dbus = false;
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            name = "x11-alacritty";
 | 
			
		||||
            verbose = true;
 | 
			
		||||
            share = pkgs.alacritty;
 | 
			
		||||
            packages = [ pkgs.alacritty ];
 | 
			
		||||
            command = "alacritty";
 | 
			
		||||
            capability = {
 | 
			
		||||
              wayland = false;
 | 
			
		||||
              x11 = true;
 | 
			
		||||
              dbus = false;
 | 
			
		||||
              pulse = false;
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        ];
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      imports = [
 | 
			
		||||
@ -212,18 +176,16 @@ nixosTest {
 | 
			
		||||
        machine.screenshot(name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def check_state(name, enablements):
 | 
			
		||||
    def check_state(command, enablements):
 | 
			
		||||
        instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
 | 
			
		||||
        if len(instances) != 1:
 | 
			
		||||
            raise Exception(f"unexpected state length {len(instances)}")
 | 
			
		||||
        instance = next(iter(instances.values()))
 | 
			
		||||
 | 
			
		||||
        config = instance['config']
 | 
			
		||||
 | 
			
		||||
        if len(config['command']) != 1 or not(config['command'][0].startswith("/nix/store/")) or not(config['command'][0].endswith(f"{name}-start")):
 | 
			
		||||
        if instance['config']['command'] != command:
 | 
			
		||||
            raise Exception(f"unexpected command {instance['config']['command']}")
 | 
			
		||||
 | 
			
		||||
        if config['confinement']['enablements'] != enablements:
 | 
			
		||||
        if instance['config']['confinement']['enablements'] != enablements:
 | 
			
		||||
            raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -250,60 +212,60 @@ nixosTest {
 | 
			
		||||
    # Create fortify uid 0 state directory:
 | 
			
		||||
    machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
 | 
			
		||||
 | 
			
		||||
    # Start fortify permissive defaults outside Wayland session:
 | 
			
		||||
    # Start fortify outside Wayland session:
 | 
			
		||||
    print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
			
		||||
 | 
			
		||||
    # Start fortify permissive defaults within Wayland session:
 | 
			
		||||
    # Start fortify within Wayland session:
 | 
			
		||||
    fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
			
		||||
    machine.wait_for_file("/tmp/dbus-done")
 | 
			
		||||
    collect_state_ui("dbus_notify_exited")
 | 
			
		||||
    machine.succeed("pkill -9 mako")
 | 
			
		||||
 | 
			
		||||
    # Start app (foot) with Wayland enablement:
 | 
			
		||||
    swaymsg("exec ne-foot")
 | 
			
		||||
    wait_for_window("u0_a1@machine")
 | 
			
		||||
    # Start a terminal (foot) within fortify:
 | 
			
		||||
    fortify("run --wayland foot")
 | 
			
		||||
    wait_for_window("u0_a0@machine")
 | 
			
		||||
    machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
 | 
			
		||||
    collect_state_ui("foot_wayland")
 | 
			
		||||
    check_state("ne-foot", 1)
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
 | 
			
		||||
    collect_state_ui("foot_wayland_permissive")
 | 
			
		||||
    check_state(["foot"], 1)
 | 
			
		||||
    # Verify acl on XDG_RUNTIME_DIR:
 | 
			
		||||
    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
 | 
			
		||||
    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000"))
 | 
			
		||||
    machine.send_chars("exit\n")
 | 
			
		||||
    machine.wait_until_fails("pgrep foot")
 | 
			
		||||
    # Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
			
		||||
    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
			
		||||
    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
 | 
			
		||||
 | 
			
		||||
    # Start app (foot) with Wayland enablement from a terminal:
 | 
			
		||||
    swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
			
		||||
    wait_for_window("u0_a1@machine")
 | 
			
		||||
    # Start a terminal (foot) within fortify from a terminal:
 | 
			
		||||
    swaymsg("exec foot $SHELL -c '(fortify run --wayland foot) & sleep 1 && fortify show --short $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
			
		||||
    wait_for_window("u0_a0@machine")
 | 
			
		||||
    machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term")
 | 
			
		||||
    machine.wait_for_file("/tmp/ps-show-ok")
 | 
			
		||||
    collect_state_ui("foot_wayland_term")
 | 
			
		||||
    check_state("ne-foot", 1)
 | 
			
		||||
    collect_state_ui("foot_wayland_permissive_term")
 | 
			
		||||
    check_state(["foot"], 1)
 | 
			
		||||
    machine.send_chars("exit\n")
 | 
			
		||||
    wait_for_window("foot")
 | 
			
		||||
    machine.send_key("ctrl-c")
 | 
			
		||||
    machine.wait_until_fails("pgrep foot")
 | 
			
		||||
 | 
			
		||||
    # Test PulseAudio (fortify does not support PipeWire yet):
 | 
			
		||||
    swaymsg("exec pa-foot")
 | 
			
		||||
    wait_for_window("u0_a2@machine")
 | 
			
		||||
    fortify("run --wayland --pulse foot")
 | 
			
		||||
    wait_for_window("u0_a0@machine")
 | 
			
		||||
    machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-pulse")
 | 
			
		||||
    collect_state_ui("pulse_wayland")
 | 
			
		||||
    check_state("pa-foot", 9)
 | 
			
		||||
    check_state(["foot"], 9)
 | 
			
		||||
    machine.send_chars("exit\n")
 | 
			
		||||
    machine.wait_until_fails("pgrep foot")
 | 
			
		||||
 | 
			
		||||
    # Test XWayland (foot does not support X):
 | 
			
		||||
    swaymsg("exec x11-alacritty")
 | 
			
		||||
    wait_for_window("u0_a3@machine")
 | 
			
		||||
    fortify("run -X alacritty")
 | 
			
		||||
    wait_for_window("u0_a0@machine")
 | 
			
		||||
    machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
 | 
			
		||||
    collect_state_ui("alacritty_x11")
 | 
			
		||||
    check_state("x11-alacritty", 2)
 | 
			
		||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11")
 | 
			
		||||
    collect_state_ui("alacritty_x11_permissive")
 | 
			
		||||
    check_state(["alacritty"], 2)
 | 
			
		||||
    machine.send_chars("exit\n")
 | 
			
		||||
    machine.wait_until_fails("pgrep alacritty")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user