Compare commits
	
		
			23 Commits
		
	
	
		
			1ec901f79e
			...
			7106b00968
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7106b00968 | |||
| 96d5d8a396 | |||
| 8a00a83c71 | |||
| 134247b57d | |||
| b5bb7654da | |||
| cc1efa22e2 | |||
| 580128922b | |||
| 23e1152baa | |||
| 8c51012ef5 | |||
| 5a64cdaf4f | |||
| a30f5e1226 | |||
| 9a239fa1a5 | |||
| 82029948e6 | |||
| dfcdc5ce20 | |||
| fa0616b274 | |||
| 20a3d4c458 | |||
| 3df344828f | |||
| 27f5922d5c | |||
| 2cf1f46ea2 | |||
| 3c55fc8e86 | |||
| eb0ef2d115 | |||
| 2f70506865 | |||
| cae567c109 | 
@ -38,6 +38,13 @@ type bundleInfo struct {
 | 
				
			|||||||
	// passed through to [fst.Config]
 | 
						// passed through to [fst.Config]
 | 
				
			||||||
	Enablements system.Enablements `json:"enablements"`
 | 
						Enablements system.Enablements `json:"enablements"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// passed through inverted to [bwrap.SyscallPolicy]
 | 
				
			||||||
 | 
						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
 | 
						// allow gpu access within sandbox
 | 
				
			||||||
	GPU bool `json:"gpu"`
 | 
						GPU bool `json:"gpu"`
 | 
				
			||||||
	// store path to nixGL mesa wrappers
 | 
						// store path to nixGL mesa wrappers
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,6 +97,7 @@ func actionStart(args []string) {
 | 
				
			|||||||
				UserNS:        app.UserNS,
 | 
									UserNS:        app.UserNS,
 | 
				
			||||||
				Net:           app.Net,
 | 
									Net:           app.Net,
 | 
				
			||||||
				Dev:           app.Dev,
 | 
									Dev:           app.Dev,
 | 
				
			||||||
 | 
									Syscall:       &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
 | 
				
			||||||
				NoNewSession:  app.NoNewSession || dropShell,
 | 
									NoNewSession:  app.NoNewSession || dropShell,
 | 
				
			||||||
				MapRealUID:    app.MapRealUID,
 | 
									MapRealUID:    app.MapRealUID,
 | 
				
			||||||
				DirectWayland: app.DirectWayland,
 | 
									DirectWayland: app.DirectWayland,
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -34,6 +35,7 @@ func withNixDaemon(
 | 
				
			|||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname:     formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
				UserNS:       true, // nix sandbox requires userns
 | 
									UserNS:       true, // nix sandbox requires userns
 | 
				
			||||||
				Net:          net,
 | 
									Net:          net,
 | 
				
			||||||
 | 
									Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									NoNewSession: dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
										{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
 | 
				
			||||||
@ -65,6 +67,7 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
 | 
				
			|||||||
			Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
								Outer:    pathSet.cacheDir, // this also ensures cacheDir via shim
 | 
				
			||||||
			Sandbox: &fst.SandboxConfig{
 | 
								Sandbox: &fst.SandboxConfig{
 | 
				
			||||||
				Hostname:     formatHostname(app.Name) + "-" + action,
 | 
									Hostname:     formatHostname(app.Name) + "-" + action,
 | 
				
			||||||
 | 
									Syscall:      &bwrap.SyscallPolicy{Multiarch: true},
 | 
				
			||||||
				NoNewSession: dropShell,
 | 
									NoNewSession: dropShell,
 | 
				
			||||||
				Filesystem: []*fst.FilesystemConfig{
 | 
									Filesystem: []*fst.FilesystemConfig{
 | 
				
			||||||
					{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
										{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
 | 
				
			||||||
 | 
				
			|||||||
@ -1,69 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,64 +0,0 @@
 | 
				
			|||||||
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) {
 | 
									t.Run("unsealed start of "+id, func(t *testing.T) {
 | 
				
			||||||
					want := "proxy not sealed"
 | 
										want := "proxy not sealed"
 | 
				
			||||||
					if err := p.Start(nil, nil, sandbox); err == nil || err.Error() != want {
 | 
										if err := p.Start(nil, nil, sandbox, false); err == nil || err.Error() != want {
 | 
				
			||||||
						t.Errorf("Start() error = %v, wantErr %q",
 | 
											t.Errorf("Start() error = %v, wantErr %q",
 | 
				
			||||||
							err, errors.New(want))
 | 
												err, errors.New(want))
 | 
				
			||||||
						return
 | 
											return
 | 
				
			||||||
@ -175,7 +175,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				t.Run("sealed start of "+id, func(t *testing.T) {
 | 
									t.Run("sealed start of "+id, func(t *testing.T) {
 | 
				
			||||||
					if err := p.Start(nil, output, sandbox); err != nil {
 | 
										if err := p.Start(nil, output, sandbox, false); err != nil {
 | 
				
			||||||
						t.Fatalf("Start(nil, nil) error = %v",
 | 
											t.Fatalf("Start(nil, nil) error = %v",
 | 
				
			||||||
							err)
 | 
												err)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,7 @@ func (p *Proxy) String() string {
 | 
				
			|||||||
	return "(unsealed dbus proxy)"
 | 
						return "(unsealed dbus proxy)"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *Proxy) Bwrap() []string {
 | 
					func (p *Proxy) BwrapStatic() []string {
 | 
				
			||||||
	return p.bwrap.Args()
 | 
						return p.bwrap.Args()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Start launches the D-Bus proxy and sets up the Wait method.
 | 
					// Start launches the D-Bus proxy and sets up the Wait method.
 | 
				
			||||||
// ready should be buffered and must only be received from once.
 | 
					// ready should be buffered and must only be received from once.
 | 
				
			||||||
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
 | 
					func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool) error {
 | 
				
			||||||
	p.lock.Lock()
 | 
						p.lock.Lock()
 | 
				
			||||||
	defer p.lock.Unlock()
 | 
						defer p.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,11 +67,16 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
 | 
				
			|||||||
			Unshare:       nil,
 | 
								Unshare:       nil,
 | 
				
			||||||
			Hostname:      "fortify-dbus",
 | 
								Hostname:      "fortify-dbus",
 | 
				
			||||||
			Chdir:         "/",
 | 
								Chdir:         "/",
 | 
				
			||||||
 | 
								Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
				
			||||||
			Clearenv:      true,
 | 
								Clearenv:      true,
 | 
				
			||||||
			NewSession:    true,
 | 
								NewSession:    true,
 | 
				
			||||||
			DieWithParent: true,
 | 
								DieWithParent: true,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !seccomp {
 | 
				
			||||||
 | 
								bc.Syscall = nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// resolve proxy socket directories
 | 
							// resolve proxy socket directories
 | 
				
			||||||
		bindTarget := make(map[string]struct{}, 2)
 | 
							bindTarget := make(map[string]struct{}, 2)
 | 
				
			||||||
		for _, ps := range []string{p.session[1], p.system[1]} {
 | 
							for _, ps := range []string{p.session[1], p.system[1]} {
 | 
				
			||||||
@ -110,7 +115,7 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
 | 
				
			|||||||
			bc.Bind(k, k)
 | 
								bc.Bind(k, k)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h = helper.MustNewBwrap(bc, p.seal, toolPath, argF)
 | 
							h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
 | 
				
			||||||
		cmd = h.Unwrap()
 | 
							cmd = h.Unwrap()
 | 
				
			||||||
		p.bwrap = bc
 | 
							p.bwrap = bc
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/install.sh
									
									
									
									
										vendored
									
									
								
							@ -4,8 +4,6 @@ cd "$(dirname -- "$0")" || exit 1
 | 
				
			|||||||
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
 | 
					install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
 | 
				
			||||||
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
 | 
					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"
 | 
					install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
 | 
				
			||||||
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
 | 
					if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
 | 
				
			||||||
    install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
 | 
					    install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
 | 
				
			||||||
 | 
				
			|||||||
@ -132,6 +132,7 @@
 | 
				
			|||||||
                [
 | 
					                [
 | 
				
			||||||
                  musl
 | 
					                  musl
 | 
				
			||||||
                  libffi
 | 
					                  libffi
 | 
				
			||||||
 | 
					                  libseccomp
 | 
				
			||||||
                  acl
 | 
					                  acl
 | 
				
			||||||
                  wayland
 | 
					                  wayland
 | 
				
			||||||
                  wayland-protocols
 | 
					                  wayland-protocols
 | 
				
			||||||
@ -172,6 +173,7 @@
 | 
				
			|||||||
                [
 | 
					                [
 | 
				
			||||||
                  musl
 | 
					                  musl
 | 
				
			||||||
                  libffi
 | 
					                  libffi
 | 
				
			||||||
 | 
					                  libseccomp
 | 
				
			||||||
                  acl
 | 
					                  acl
 | 
				
			||||||
                  wayland
 | 
					                  wayland
 | 
				
			||||||
                  wayland-protocols
 | 
					                  wayland-protocols
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,13 @@ package fst
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/system"
 | 
						"git.gensokyo.uk/security/fortify/internal/system"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Tmp = "/.fortify"
 | 
					const Tmp = "/.fortify"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Config is used to seal an *App
 | 
					// Config is used to seal an app
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	// application ID
 | 
						// application ID
 | 
				
			||||||
	ID string `json:"id"`
 | 
						ID string `json:"id"`
 | 
				
			||||||
@ -107,9 +108,10 @@ func Template() *Config {
 | 
				
			|||||||
				Hostname:      "localhost",
 | 
									Hostname:      "localhost",
 | 
				
			||||||
				UserNS:        true,
 | 
									UserNS:        true,
 | 
				
			||||||
				Net:           true,
 | 
									Net:           true,
 | 
				
			||||||
 | 
									Dev:           true,
 | 
				
			||||||
 | 
									Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
				
			||||||
				NoNewSession:  true,
 | 
									NoNewSession:  true,
 | 
				
			||||||
				MapRealUID:    true,
 | 
									MapRealUID:    true,
 | 
				
			||||||
				Dev:           true,
 | 
					 | 
				
			||||||
				DirectWayland: false,
 | 
									DirectWayland: false,
 | 
				
			||||||
				// example API credentials pulled from Google Chrome
 | 
									// example API credentials pulled from Google Chrome
 | 
				
			||||||
				// DO NOT USE THESE IN A REAL BROWSER
 | 
									// DO NOT USE THESE IN A REAL BROWSER
 | 
				
			||||||
@ -123,7 +125,8 @@ func Template() *Config {
 | 
				
			|||||||
					{Src: "/run/current-system"},
 | 
										{Src: "/run/current-system"},
 | 
				
			||||||
					{Src: "/run/opengl-driver"},
 | 
										{Src: "/run/opengl-driver"},
 | 
				
			||||||
					{Src: "/var/db/nix-channels"},
 | 
										{Src: "/var/db/nix-channels"},
 | 
				
			||||||
					{Src: "/home/chronos", Write: true, Must: true},
 | 
										{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
 | 
				
			||||||
 | 
											Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
 | 
				
			||||||
					{Src: "/dev/dri", Device: true},
 | 
										{Src: "/dev/dri", Device: true},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
									Link:     [][2]string{{"/run/user/65534", "/run/user/150"}},
 | 
				
			||||||
@ -131,6 +134,10 @@ func Template() *Config {
 | 
				
			|||||||
				AutoEtc:  true,
 | 
									AutoEtc:  true,
 | 
				
			||||||
				Override: []string{"/var/run/nscd"},
 | 
									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{
 | 
								SystemBus: &dbus.Config{
 | 
				
			||||||
				See:       nil,
 | 
									See:       nil,
 | 
				
			||||||
				Talk:      []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
									Talk:      []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,8 @@ type SandboxConfig struct {
 | 
				
			|||||||
	Net bool `json:"net,omitempty"`
 | 
						Net bool `json:"net,omitempty"`
 | 
				
			||||||
	// share all devices
 | 
						// share all devices
 | 
				
			||||||
	Dev bool `json:"dev,omitempty"`
 | 
						Dev bool `json:"dev,omitempty"`
 | 
				
			||||||
 | 
						// seccomp syscall filter policy
 | 
				
			||||||
 | 
						Syscall *bwrap.SyscallPolicy `json:"syscall"`
 | 
				
			||||||
	// do not run in new session
 | 
						// do not run in new session
 | 
				
			||||||
	NoNewSession bool `json:"no_new_session,omitempty"`
 | 
						NoNewSession bool `json:"no_new_session,omitempty"`
 | 
				
			||||||
	// map target user uid to privileged user uid in the user namespace
 | 
						// map target user uid to privileged user uid in the user namespace
 | 
				
			||||||
@ -50,6 +52,10 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
				
			|||||||
		return nil, errors.New("nil sandbox config")
 | 
							return nil, errors.New("nil sandbox config")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if s.Syscall == nil {
 | 
				
			||||||
 | 
							fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var uid int
 | 
						var uid int
 | 
				
			||||||
	if !s.MapRealUID {
 | 
						if !s.MapRealUID {
 | 
				
			||||||
		uid = 65534
 | 
							uid = 65534
 | 
				
			||||||
@ -69,6 +75,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
 | 
				
			|||||||
		so this capacity should eliminate copies for most setups */
 | 
							so this capacity should eliminate copies for most setups */
 | 
				
			||||||
		Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
							Filesystem: make([]bwrap.FSBuilder, 0, 256),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Syscall:       s.Syscall,
 | 
				
			||||||
		NewSession:    !s.NoNewSession,
 | 
							NewSession:    !s.NoNewSession,
 | 
				
			||||||
		DieWithParent: true,
 | 
							DieWithParent: true,
 | 
				
			||||||
		AsInit:        true,
 | 
							AsInit:        true,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,6 @@ import (
 | 
				
			|||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/proc"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// BubblewrapName is the file name or path to bubblewrap.
 | 
					// BubblewrapName is the file name or path to bubblewrap.
 | 
				
			||||||
@ -21,8 +20,6 @@ type bubblewrap struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// bwrap pipes
 | 
						// bwrap pipes
 | 
				
			||||||
	control *pipes
 | 
						control *pipes
 | 
				
			||||||
	// sync pipe
 | 
					 | 
				
			||||||
	sync *os.File
 | 
					 | 
				
			||||||
	// returns an array of arguments passed directly
 | 
						// returns an array of arguments passed directly
 | 
				
			||||||
	// to the child process spawned by bwrap
 | 
						// to the child process spawned by bwrap
 | 
				
			||||||
	argF func(argsFD, statFD int) []string
 | 
						argF func(argsFD, statFD int) []string
 | 
				
			||||||
@ -49,11 +46,6 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
 | 
				
			|||||||
		return errors.New("exec: already started")
 | 
							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
 | 
						// prepare bwrap pipe and args
 | 
				
			||||||
	if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
 | 
						if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -119,8 +111,13 @@ func (b *bubblewrap) Unwrap() *exec.Cmd {
 | 
				
			|||||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					// 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.
 | 
					// 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.
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper {
 | 
					func MustNewBwrap(
 | 
				
			||||||
	b, err := NewBwrap(conf, wt, name, argF)
 | 
						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)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		panic(err.Error())
 | 
							panic(err.Error())
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -131,22 +128,30 @@ func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(arg
 | 
				
			|||||||
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
 | 
					// 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.
 | 
					// 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.
 | 
					// Function argF returns an array of arguments passed directly to the child process.
 | 
				
			||||||
func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) (Helper, error) {
 | 
					func NewBwrap(
 | 
				
			||||||
 | 
						conf *bwrap.Config, name string,
 | 
				
			||||||
 | 
						wt io.WriterTo, argF func(argsFD, statFD int) []string,
 | 
				
			||||||
 | 
						extraFiles []*os.File,
 | 
				
			||||||
 | 
						syncFd *os.File,
 | 
				
			||||||
 | 
					) (Helper, error) {
 | 
				
			||||||
	b := new(bubblewrap)
 | 
						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.argF = argF
 | 
				
			||||||
	b.name = name
 | 
						b.name = name
 | 
				
			||||||
	if wt != nil {
 | 
						if wt != nil {
 | 
				
			||||||
		b.controlPt = &pipes{args: wt}
 | 
							b.controlPt = &pipes{args: wt}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	b.Cmd = execCommand(BubblewrapName)
 | 
						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
 | 
						return b, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,13 @@
 | 
				
			|||||||
package bwrap
 | 
					package bwrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "encoding/gob"
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/internal/proc"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Builder interface {
 | 
					type Builder interface {
 | 
				
			||||||
	Len() int
 | 
						Len() int
 | 
				
			||||||
@ -12,6 +19,11 @@ type FSBuilder interface {
 | 
				
			|||||||
	Builder
 | 
						Builder
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FDBuilder interface {
 | 
				
			||||||
 | 
						Len() int
 | 
				
			||||||
 | 
						Append(args *[]string, extraFiles *[]*os.File) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	gob.Register(new(pairF))
 | 
						gob.Register(new(pairF))
 | 
				
			||||||
	gob.Register(new(stringF))
 | 
						gob.Register(new(stringF))
 | 
				
			||||||
@ -45,6 +57,33 @@ func (s stringF) Append(args *[]string) {
 | 
				
			|||||||
	*args = append(*args, s[0], s[1])
 | 
						*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.
 | 
					// Args returns a slice of bwrap args corresponding to c.
 | 
				
			||||||
func (c *Config) Args() (args []string) {
 | 
					func (c *Config) Args() (args []string) {
 | 
				
			||||||
	builders := []Builder{
 | 
						builders := []Builder{
 | 
				
			||||||
@ -75,3 +114,25 @@ func (c *Config) Args() (args []string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return
 | 
						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,10 +161,3 @@ func (c *Config) SetGID(gid int) *Config {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return c
 | 
						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,9 +1,5 @@
 | 
				
			|||||||
package bwrap
 | 
					package bwrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	// unshare every namespace we support by default if nil
 | 
						// unshare every namespace we support by default if nil
 | 
				
			||||||
	// (--unshare-all)
 | 
						// (--unshare-all)
 | 
				
			||||||
@ -51,6 +47,10 @@ type Config struct {
 | 
				
			|||||||
	// (--chmod OCTAL PATH)
 | 
						// (--chmod OCTAL PATH)
 | 
				
			||||||
	Chmod ChmodConfig `json:"chmod,omitempty"`
 | 
						Chmod ChmodConfig `json:"chmod,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// load and use seccomp rules from FD (not repeatable)
 | 
				
			||||||
 | 
						// (--seccomp FD)
 | 
				
			||||||
 | 
						Syscall *SyscallPolicy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// create a new terminal session
 | 
						// create a new terminal session
 | 
				
			||||||
	// (--new-session)
 | 
						// (--new-session)
 | 
				
			||||||
	NewSession bool `json:"new_session"`
 | 
						NewSession bool `json:"new_session"`
 | 
				
			||||||
@ -61,10 +61,6 @@ type Config struct {
 | 
				
			|||||||
	// (--as-pid-1)
 | 
						// (--as-pid-1)
 | 
				
			||||||
	AsInit bool `json:"as_init"`
 | 
						AsInit bool `json:"as_init"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// keep this fd open while sandbox is running
 | 
					 | 
				
			||||||
	// (--sync-fd FD)
 | 
					 | 
				
			||||||
	sync *os.File
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/* unmapped options include:
 | 
						/* unmapped options include:
 | 
				
			||||||
	    --unshare-user-try           Create new user namespace if possible else continue by skipping it
 | 
						    --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
 | 
						    --unshare-cgroup-try         Create new cgroup namespace if possible else continue by skipping it
 | 
				
			||||||
@ -78,7 +74,6 @@ type Config struct {
 | 
				
			|||||||
	    --file FD DEST               Copy from FD to destination DEST
 | 
						    --file FD DEST               Copy from FD to destination DEST
 | 
				
			||||||
	    --bind-data FD DEST          Copy from FD to file which is bind-mounted on 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
 | 
						    --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)
 | 
						    --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
 | 
						    --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
 | 
						    --userns-block-fd FD         Block on FD until the user namespace is ready
 | 
				
			||||||
@ -90,12 +85,6 @@ type Config struct {
 | 
				
			|||||||
	among which --args is used internally for passing arguments */
 | 
						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 {
 | 
					type UnshareConfig struct {
 | 
				
			||||||
	// (--unshare-user)
 | 
						// (--unshare-user)
 | 
				
			||||||
	// create new user namespace
 | 
						// create new user namespace
 | 
				
			||||||
 | 
				
			|||||||
@ -126,8 +126,7 @@ func TestConfig_Args(t *testing.T) {
 | 
				
			|||||||
			name: "uid gid sync",
 | 
								name: "uid gid sync",
 | 
				
			||||||
			conf: (new(bwrap.Config)).
 | 
								conf: (new(bwrap.Config)).
 | 
				
			||||||
				SetUID(1971).
 | 
									SetUID(1971).
 | 
				
			||||||
				SetGID(100).
 | 
									SetGID(100),
 | 
				
			||||||
				SetSync(os.Stdin),
 | 
					 | 
				
			||||||
			want: []string{
 | 
								want: []string{
 | 
				
			||||||
				"--unshare-all", "--unshare-user",
 | 
									"--unshare-all", "--unshare-user",
 | 
				
			||||||
				"--disable-userns", "--assert-userns-disabled",
 | 
									"--disable-userns", "--assert-userns-disabled",
 | 
				
			||||||
@ -135,8 +134,6 @@ func TestConfig_Args(t *testing.T) {
 | 
				
			|||||||
				"--uid", "1971",
 | 
									"--uid", "1971",
 | 
				
			||||||
				// SetGID(100)
 | 
									// SetGID(100)
 | 
				
			||||||
				"--gid", "100",
 | 
									"--gid", "100",
 | 
				
			||||||
				// SetSync(os.Stdin)
 | 
					 | 
				
			||||||
				// this is set when the process is created
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -246,10 +243,4 @@ func TestConfig_Args(t *testing.T) {
 | 
				
			|||||||
		}()
 | 
							}()
 | 
				
			||||||
		(new(bwrap.Config)).Persist("/run", "", "")
 | 
							(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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										254
									
								
								helper/bwrap/seccomp-export.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								helper/bwrap/seccomp-export.c
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,254 @@
 | 
				
			|||||||
 | 
					#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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								helper/bwrap/seccomp-export.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								helper/bwrap/seccomp-export.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					#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);
 | 
				
			||||||
							
								
								
									
										95
									
								
								helper/bwrap/seccomp-resolve.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								helper/bwrap/seccomp-resolve.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								helper/bwrap/seccomp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								helper/bwrap/seccomp.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					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,6 +43,9 @@ const (
 | 
				
			|||||||
	Overlay
 | 
						Overlay
 | 
				
			||||||
	TmpOverlay
 | 
						TmpOverlay
 | 
				
			||||||
	ROOverlay
 | 
						ROOverlay
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						SyncFd
 | 
				
			||||||
 | 
						Seccomp
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var positionalArgs = [...]string{
 | 
					var positionalArgs = [...]string{
 | 
				
			||||||
@ -70,6 +73,9 @@ var positionalArgs = [...]string{
 | 
				
			|||||||
	Overlay:    "--overlay",
 | 
						Overlay:    "--overlay",
 | 
				
			||||||
	TmpOverlay: "--tmp-overlay",
 | 
						TmpOverlay: "--tmp-overlay",
 | 
				
			||||||
	ROOverlay:  "--ro-overlay",
 | 
						ROOverlay:  "--ro-overlay",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						SyncFd:  "--sync-fd",
 | 
				
			||||||
 | 
						Seccomp: "--seccomp",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PermConfig[T FSBuilder] struct {
 | 
					type PermConfig[T FSBuilder] struct {
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,11 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
			helper.BubblewrapName = bubblewrapName
 | 
								helper.BubblewrapName = bubblewrapName
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h := helper.MustNewBwrap(sc, argsWt, "fortify", argF)
 | 
							h := helper.MustNewBwrap(
 | 
				
			||||||
 | 
								sc, "fortify",
 | 
				
			||||||
 | 
								argsWt, argF,
 | 
				
			||||||
 | 
								nil, nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
 | 
							if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
			t.Errorf("Start() error = %v, wantErr %v",
 | 
								t.Errorf("Start() error = %v, wantErr %v",
 | 
				
			||||||
@ -40,7 +44,11 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("valid new helper nil check", func(t *testing.T) {
 | 
						t.Run("valid new helper nil check", func(t *testing.T) {
 | 
				
			||||||
		if got := helper.MustNewBwrap(sc, argsWt, "fortify", argF); got == nil {
 | 
							if got := helper.MustNewBwrap(
 | 
				
			||||||
 | 
								sc, "fortify",
 | 
				
			||||||
 | 
								argsWt, argF,
 | 
				
			||||||
 | 
								nil, nil,
 | 
				
			||||||
 | 
							); got == nil {
 | 
				
			||||||
			t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
 | 
								t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
 | 
				
			||||||
				sc, argsWt, "fortify")
 | 
									sc, argsWt, "fortify")
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@ -56,7 +64,11 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}()
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		helper.MustNewBwrap(&bwrap.Config{Hostname: "\x00"}, nil, "fortify", argF)
 | 
							helper.MustNewBwrap(
 | 
				
			||||||
 | 
								&bwrap.Config{Hostname: "\x00"}, "fortify",
 | 
				
			||||||
 | 
								nil, argF,
 | 
				
			||||||
 | 
								nil, nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start notify without pipes panic", func(t *testing.T) {
 | 
						t.Run("start notify without pipes panic", func(t *testing.T) {
 | 
				
			||||||
@ -69,13 +81,21 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
		}()
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		panic(fmt.Sprintf("unreachable: %v",
 | 
							panic(fmt.Sprintf("unreachable: %v",
 | 
				
			||||||
			helper.MustNewBwrap(sc, nil, "fortify", argF).StartNotify(make(chan error))))
 | 
								helper.MustNewBwrap(
 | 
				
			||||||
 | 
									sc, "fortify",
 | 
				
			||||||
 | 
									nil, argF,
 | 
				
			||||||
 | 
									nil, nil,
 | 
				
			||||||
 | 
								).StartNotify(make(chan error))))
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("start without pipes", func(t *testing.T) {
 | 
						t.Run("start without pipes", func(t *testing.T) {
 | 
				
			||||||
		helper.InternalReplaceExecCommand(t)
 | 
							helper.InternalReplaceExecCommand(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		h := helper.MustNewBwrap(sc, nil, "crash-test-dummy", argFChecked)
 | 
							h := helper.MustNewBwrap(
 | 
				
			||||||
 | 
								sc, "crash-test-dummy",
 | 
				
			||||||
 | 
								nil, argFChecked,
 | 
				
			||||||
 | 
								nil, nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		cmd := h.Unwrap()
 | 
							cmd := h.Unwrap()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
							stdout, stderr := new(strings.Builder), new(strings.Builder)
 | 
				
			||||||
@ -107,6 +127,6 @@ func TestBwrap(t *testing.T) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("implementation compliance", func(t *testing.T) {
 | 
						t.Run("implementation compliance", func(t *testing.T) {
 | 
				
			||||||
		testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, argsWt, "crash-test-dummy", argF) })
 | 
							testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ package app
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"sync/atomic"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
						"git.gensokyo.uk/security/fortify/internal/linux"
 | 
				
			||||||
@ -30,9 +29,6 @@ type RunState struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type app struct {
 | 
					type app struct {
 | 
				
			||||||
	// single-use config reference
 | 
					 | 
				
			||||||
	ct *appCt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// application unique identifier
 | 
						// application unique identifier
 | 
				
			||||||
	id *fst.ID
 | 
						id *fst.ID
 | 
				
			||||||
	// operating system interface
 | 
						// operating system interface
 | 
				
			||||||
@ -74,24 +70,3 @@ func New(os linux.System) (App, error) {
 | 
				
			|||||||
	a.os = os
 | 
						a.os = os
 | 
				
			||||||
	return a, fst.NewAppID(a.id)
 | 
						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,6 +39,7 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			Net:      true,
 | 
								Net:      true,
 | 
				
			||||||
			UserNS:   true,
 | 
								UserNS:   true,
 | 
				
			||||||
			Clearenv: true,
 | 
								Clearenv: true,
 | 
				
			||||||
 | 
								Syscall:  new(bwrap.SyscallPolicy),
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Chdir:    "/home/chronos",
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
								SetEnv: map[string]string{
 | 
				
			||||||
				"HOME":              "/home/chronos",
 | 
									"HOME":              "/home/chronos",
 | 
				
			||||||
@ -258,6 +259,7 @@ var testCasesPd = []sealTestCase{
 | 
				
			|||||||
			UserNS:   true,
 | 
								UserNS:   true,
 | 
				
			||||||
			Chdir:    "/home/chronos",
 | 
								Chdir:    "/home/chronos",
 | 
				
			||||||
			Clearenv: true,
 | 
								Clearenv: true,
 | 
				
			||||||
 | 
								Syscall:  new(bwrap.SyscallPolicy),
 | 
				
			||||||
			SetEnv: map[string]string{
 | 
								SetEnv: map[string]string{
 | 
				
			||||||
				"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
									"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
 | 
				
			||||||
				"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
									"DBUS_SYSTEM_BUS_ADDRESS":  "unix:path=/run/dbus/system_bus_socket",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,11 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
@ -11,6 +14,7 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/linux"
 | 
						"git.gensokyo.uk/security/fortify/internal/linux"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/state"
 | 
						"git.gensokyo.uk/security/fortify/internal/state"
 | 
				
			||||||
@ -47,6 +51,8 @@ type appSeal struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// pass-through enablement tracking from config
 | 
						// pass-through enablement tracking from config
 | 
				
			||||||
	et system.Enablements
 | 
						et system.Enablements
 | 
				
			||||||
 | 
						// initial config gob encoding buffer
 | 
				
			||||||
 | 
						ct io.WriterTo
 | 
				
			||||||
	// wayland socket direct access
 | 
						// wayland socket direct access
 | 
				
			||||||
	directWayland bool
 | 
						directWayland bool
 | 
				
			||||||
	// extra UpdatePerm ops
 | 
						// extra UpdatePerm ops
 | 
				
			||||||
@ -85,6 +91,14 @@ func (a *app) Seal(config *fst.Config) error {
 | 
				
			|||||||
	// create seal
 | 
						// create seal
 | 
				
			||||||
	seal := new(appSeal)
 | 
						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
 | 
						// fetch system constants
 | 
				
			||||||
	seal.Paths = a.os.Paths()
 | 
						seal.Paths = a.os.Paths()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -181,6 +195,7 @@ func (a *app) Seal(config *fst.Config) error {
 | 
				
			|||||||
		conf := &fst.SandboxConfig{
 | 
							conf := &fst.SandboxConfig{
 | 
				
			||||||
			UserNS:       true,
 | 
								UserNS:       true,
 | 
				
			||||||
			Net:          true,
 | 
								Net:          true,
 | 
				
			||||||
 | 
								Syscall:      new(bwrap.SyscallPolicy),
 | 
				
			||||||
			NoNewSession: true,
 | 
								NoNewSession: true,
 | 
				
			||||||
			AutoEtc:      true,
 | 
								AutoEtc:      true,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -252,6 +267,5 @@ func (a *app) Seal(config *fst.Config) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// seal app and release lock
 | 
						// seal app and release lock
 | 
				
			||||||
	a.seal = seal
 | 
						a.seal = seal
 | 
				
			||||||
	a.ct = newAppCt(config)
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/acl"
 | 
						"git.gensokyo.uk/security/fortify/acl"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
@ -240,7 +241,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
 | 
				
			|||||||
		// publish current user's pulse cookie for target user
 | 
							// publish current user's pulse cookie for target user
 | 
				
			||||||
		if src, err := discoverPulseCookie(os); err != nil {
 | 
							if src, err := discoverPulseCookie(os); err != nil {
 | 
				
			||||||
			// not fatal
 | 
								// not fatal
 | 
				
			||||||
			fmsg.VPrintln(err.(*fmsg.BaseError).Message())
 | 
								fmsg.VPrintln(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			dst := path.Join(seal.share, "pulse-cookie")
 | 
								dst := path.Join(seal.share, "pulse-cookie")
 | 
				
			||||||
			innerDst := fst.Tmp + "/pulse-cookie"
 | 
								innerDst := fst.Tmp + "/pulse-cookie"
 | 
				
			||||||
 | 
				
			|||||||
@ -45,33 +45,20 @@ 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
 | 
						// startup will go ahead, commit system setup
 | 
				
			||||||
	if err := a.seal.sys.Commit(); err != nil {
 | 
						if err := a.seal.sys.Commit(); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	a.seal.sys.needRevert = true
 | 
						a.seal.sys.needRevert = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// export sync pipe from sys
 | 
					 | 
				
			||||||
	a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// start shim via manager
 | 
						// start shim via manager
 | 
				
			||||||
 | 
						a.shim = new(shim.Shim)
 | 
				
			||||||
	waitErr := make(chan error, 1)
 | 
						waitErr := make(chan error, 1)
 | 
				
			||||||
	if startTime, err := a.shim.Start(); err != nil {
 | 
						if startTime, err := a.shim.Start(
 | 
				
			||||||
 | 
							a.seal.sys.user.as,
 | 
				
			||||||
 | 
							a.seal.sys.user.supp,
 | 
				
			||||||
 | 
							a.seal.sys.Sync(),
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// shim process created
 | 
							// shim process created
 | 
				
			||||||
@ -88,7 +75,14 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
 | 
				
			|||||||
		}()
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// send payload
 | 
							// send payload
 | 
				
			||||||
		if err = a.shim.Serve(shimSetupCtx); err != nil {
 | 
							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 {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,14 +90,13 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
 | 
				
			|||||||
		sd := state.State{
 | 
							sd := state.State{
 | 
				
			||||||
			ID:   *a.id,
 | 
								ID:   *a.id,
 | 
				
			||||||
			PID:  a.shim.Unwrap().Process.Pid,
 | 
								PID:  a.shim.Unwrap().Process.Pid,
 | 
				
			||||||
			Config: a.ct.Unwrap(),
 | 
					 | 
				
			||||||
			Time: *startTime,
 | 
								Time: *startTime,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// register process state
 | 
							// register process state
 | 
				
			||||||
		var err0 = new(StateStoreError)
 | 
							var err0 = new(StateStoreError)
 | 
				
			||||||
		err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
 | 
							err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
 | 
				
			||||||
			err0.InnerErr = c.Save(&sd)
 | 
								err0.InnerErr = c.Save(&sd, a.seal.ct)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		a.seal.sys.saveState = true
 | 
							a.seal.sys.saveState = true
 | 
				
			||||||
		if err = err0.equiv("cannot save process state:"); err != nil {
 | 
							if err = err0.equiv("cannot save process state:"); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,12 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
 | 
					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.
 | 
						// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
 | 
				
			||||||
	fd = uintptr(3 + len(cmd.ExtraFiles))
 | 
						fd = uintptr(3 + len(*extraFiles))
 | 
				
			||||||
	cmd.ExtraFiles = append(cmd.ExtraFiles, f)
 | 
						*extraFiles = append(*extraFiles, f)
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,10 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/helper"
 | 
						"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"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/proc"
 | 
						"git.gensokyo.uk/security/fortify/internal/proc"
 | 
				
			||||||
@ -29,14 +29,6 @@ func Main() {
 | 
				
			|||||||
		panic("unreachable")
 | 
							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
 | 
						// receive setup payload
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		payload    Payload
 | 
							payload    Payload
 | 
				
			||||||
@ -62,8 +54,9 @@ func Main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// restore bwrap sync fd
 | 
						// restore bwrap sync fd
 | 
				
			||||||
 | 
						var syncFd *os.File
 | 
				
			||||||
	if payload.Sync != nil {
 | 
						if payload.Sync != nil {
 | 
				
			||||||
		payload.Bwrap.SetSync(os.NewFile(*payload.Sync, "sync"))
 | 
							syncFd = os.NewFile(*payload.Sync, "sync")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// close setup socket
 | 
						// close setup socket
 | 
				
			||||||
@ -134,17 +127,19 @@ func Main() {
 | 
				
			|||||||
	conf.Symlink("fortify", innerInit)
 | 
						conf.Symlink("fortify", innerInit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
						helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
 | 
				
			||||||
	if b, err := helper.NewBwrap(conf, nil, innerInit,
 | 
						if fmsg.Verbose() {
 | 
				
			||||||
		func(int, int) []string { return make([]string, 0) }); err != nil {
 | 
							bwrap.CPrintln = fmsg.Println
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if b, err := helper.NewBwrap(
 | 
				
			||||||
 | 
							conf, innerInit,
 | 
				
			||||||
 | 
							nil, func(int, int) []string { return make([]string, 0) },
 | 
				
			||||||
 | 
							extraFiles,
 | 
				
			||||||
 | 
							syncFd,
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
		fmsg.Fatalf("malformed sandbox config: %v", err)
 | 
							fmsg.Fatalf("malformed sandbox config: %v", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		cmd := b.Unwrap()
 | 
							cmd := b.Unwrap()
 | 
				
			||||||
		cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
							cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
		cmd.ExtraFiles = extraFiles
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if fmsg.Verbose() {
 | 
					 | 
				
			||||||
			fmsg.VPrintln("bwrap args:", conf.Args())
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// run and pass through exit code
 | 
							// run and pass through exit code
 | 
				
			||||||
		if err = b.Start(); err != nil {
 | 
							if err = b.Start(); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -20,22 +20,12 @@ import (
 | 
				
			|||||||
type Shim struct {
 | 
					type Shim struct {
 | 
				
			||||||
	// user switcher process
 | 
						// user switcher process
 | 
				
			||||||
	cmd *exec.Cmd
 | 
						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
 | 
						// fallback exit notifier with error returned killing the process
 | 
				
			||||||
	killFallback chan error
 | 
						killFallback chan error
 | 
				
			||||||
	// shim setup payload
 | 
					 | 
				
			||||||
	payload *Payload
 | 
					 | 
				
			||||||
	// monitor to shim encoder
 | 
						// monitor to shim encoder
 | 
				
			||||||
	encoder *gob.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 {
 | 
					func (s *Shim) String() string {
 | 
				
			||||||
@ -53,7 +43,14 @@ func (s *Shim) WaitFallback() chan error {
 | 
				
			|||||||
	return s.killFallback
 | 
						return s.killFallback
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Shim) Start() (*time.Time, error) {
 | 
					func (s *Shim) Start(
 | 
				
			||||||
 | 
						// string representation of application id
 | 
				
			||||||
 | 
						aid string,
 | 
				
			||||||
 | 
						// string representation of supplementary group ids
 | 
				
			||||||
 | 
						supp []string,
 | 
				
			||||||
 | 
						// bwrap --sync-fd
 | 
				
			||||||
 | 
						syncFd *os.File,
 | 
				
			||||||
 | 
					) (*time.Time, error) {
 | 
				
			||||||
	// prepare user switcher invocation
 | 
						// prepare user switcher invocation
 | 
				
			||||||
	var fsu string
 | 
						var fsu string
 | 
				
			||||||
	if p, ok := internal.Path(internal.Fsu); !ok {
 | 
						if p, ok := internal.Path(internal.Fsu); !ok {
 | 
				
			||||||
@ -72,22 +69,22 @@ func (s *Shim) Start() (*time.Time, error) {
 | 
				
			|||||||
		s.encoder = e
 | 
							s.encoder = e
 | 
				
			||||||
		s.cmd.Env = []string{
 | 
							s.cmd.Env = []string{
 | 
				
			||||||
			Env + "=" + strconv.Itoa(fd),
 | 
								Env + "=" + strconv.Itoa(fd),
 | 
				
			||||||
			"FORTIFY_APP_ID=" + s.aid,
 | 
								"FORTIFY_APP_ID=" + aid,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// format fsu supplementary groups
 | 
						// format fsu supplementary groups
 | 
				
			||||||
	if len(s.supp) > 0 {
 | 
						if len(supp) > 0 {
 | 
				
			||||||
		fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
 | 
							fmsg.VPrintf("attaching supplementary group ids %s", supp)
 | 
				
			||||||
		s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
 | 
							s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
						s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 | 
				
			||||||
	s.cmd.Dir = "/"
 | 
						s.cmd.Dir = "/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pass sync fd if set
 | 
						// pass sync fd if set
 | 
				
			||||||
	if s.payload.Bwrap.Sync() != nil {
 | 
						if syncFd != nil {
 | 
				
			||||||
		fd := proc.ExtraFile(s.cmd, s.payload.Bwrap.Sync())
 | 
							fd := proc.ExtraFile(s.cmd, syncFd)
 | 
				
			||||||
		s.payload.Sync = &fd
 | 
							s.sync = &fd
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmsg.VPrintln("starting shim via fsu:", s.cmd)
 | 
						fmsg.VPrintln("starting shim via fsu:", s.cmd)
 | 
				
			||||||
@ -101,7 +98,7 @@ func (s *Shim) Start() (*time.Time, error) {
 | 
				
			|||||||
	return &startTime, nil
 | 
						return &startTime, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Shim) Serve(ctx context.Context) error {
 | 
					func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
 | 
				
			||||||
	// kill shim if something goes wrong and an error is returned
 | 
						// kill shim if something goes wrong and an error is returned
 | 
				
			||||||
	s.killFallback = make(chan error, 1)
 | 
						s.killFallback = make(chan error, 1)
 | 
				
			||||||
	killShim := func() {
 | 
						killShim := func() {
 | 
				
			||||||
@ -111,8 +108,9 @@ func (s *Shim) Serve(ctx context.Context) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer func() { killShim() }()
 | 
						defer func() { killShim() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						payload.Sync = s.sync
 | 
				
			||||||
	encodeErr := make(chan error)
 | 
						encodeErr := make(chan error)
 | 
				
			||||||
	go func() { encodeErr <- s.encoder.Encode(s.payload) }()
 | 
						go func() { encodeErr <- s.encoder.Encode(payload) }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	select {
 | 
						select {
 | 
				
			||||||
	// encode return indicates setup completion
 | 
						// encode return indicates setup completion
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
package shim
 | 
					package shim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
					import (
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Env = "FORTIFY_SHIM"
 | 
					const Env = "FORTIFY_SHIM"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,11 @@
 | 
				
			|||||||
package state
 | 
					package state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/binary"
 | 
				
			||||||
	"encoding/gob"
 | 
						"encoding/gob"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
@ -208,12 +210,11 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
 | 
				
			|||||||
				s := new(State)
 | 
									s := new(State)
 | 
				
			||||||
				r[*id] = s
 | 
									r[*id] = s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// append regardless, but only parse if required, used to implement Len
 | 
									// append regardless, but only parse if required, implements Len
 | 
				
			||||||
				if decode {
 | 
									if decode {
 | 
				
			||||||
					if err = gob.NewDecoder(f).Decode(s); err != nil {
 | 
										if err = b.decodeState(f, s); err != nil {
 | 
				
			||||||
						return err
 | 
											return err
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					 | 
				
			||||||
					if s.ID != *id {
 | 
										if s.ID != *id {
 | 
				
			||||||
						return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID)
 | 
											return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@ -229,18 +230,65 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
 | 
				
			|||||||
	return r, nil
 | 
						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
 | 
					// Save writes process state to filesystem
 | 
				
			||||||
func (b *multiBackend) Save(state *State) error {
 | 
					func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error {
 | 
				
			||||||
	b.lock.Lock()
 | 
						b.lock.Lock()
 | 
				
			||||||
	defer b.lock.Unlock()
 | 
						defer b.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if state.Config == nil {
 | 
						if configWriter == nil && state.Config == nil {
 | 
				
			||||||
		return errors.New("state does not contain config")
 | 
							return ErrNoConfig
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	statePath := b.filename(&state.ID)
 | 
						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 {
 | 
						if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@ -250,11 +298,43 @@ func (b *multiBackend) Save(state *State) error {
 | 
				
			|||||||
				panic("state file closed prematurely")
 | 
									panic("state file closed prematurely")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}()
 | 
							}()
 | 
				
			||||||
		// encode into state file
 | 
							return b.encodeState(f, state, configWriter)
 | 
				
			||||||
		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 {
 | 
					func (b *multiBackend) Destroy(id fst.ID) error {
 | 
				
			||||||
	b.lock.Lock()
 | 
						b.lock.Lock()
 | 
				
			||||||
	defer b.lock.Unlock()
 | 
						defer b.lock.Unlock()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,15 @@
 | 
				
			|||||||
package state
 | 
					package state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var ErrNoConfig = errors.New("state does not contain config")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Entries map[fst.ID]*State
 | 
					type Entries map[fst.ID]*State
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Store interface {
 | 
					type Store interface {
 | 
				
			||||||
@ -24,13 +28,13 @@ type Store interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Cursor provides access to the store
 | 
					// Cursor provides access to the store
 | 
				
			||||||
type Cursor interface {
 | 
					type Cursor interface {
 | 
				
			||||||
	Save(state *State) error
 | 
						Save(state *State, configWriter io.WriterTo) error
 | 
				
			||||||
	Destroy(id fst.ID) error
 | 
						Destroy(id fst.ID) error
 | 
				
			||||||
	Load() (Entries, error)
 | 
						Load() (Entries, error)
 | 
				
			||||||
	Len() (int, error)
 | 
						Len() (int, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// State is the on-disk format for a fortified process's state information
 | 
					// State is a fortify process's state
 | 
				
			||||||
type State struct {
 | 
					type State struct {
 | 
				
			||||||
	// fortify instance id
 | 
						// fortify instance id
 | 
				
			||||||
	ID fst.ID `json:"instance"`
 | 
						ID fst.ID `json:"instance"`
 | 
				
			||||||
@ -40,5 +44,5 @@ type State struct {
 | 
				
			|||||||
	Config *fst.Config `json:"config"`
 | 
						Config *fst.Config `json:"config"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process start time
 | 
						// process start time
 | 
				
			||||||
	Time time.Time
 | 
						Time time.Time `json:"time"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
package state_test
 | 
					package state_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/gob"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"math/rand/v2"
 | 
						"math/rand/v2"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
@ -28,9 +31,12 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
		tl
 | 
							tl
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var tc [tl]state.State
 | 
						var tc [tl]struct {
 | 
				
			||||||
 | 
							state state.State
 | 
				
			||||||
 | 
							ct    bytes.Buffer
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	for i := 0; i < tl; i++ {
 | 
						for i := 0; i < tl; i++ {
 | 
				
			||||||
		makeState(t, &tc[i])
 | 
							makeState(t, &tc[i].state, &tc[i].ct)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	do := func(aid int, f func(c state.Cursor)) {
 | 
						do := func(aid int, f func(c state.Cursor)) {
 | 
				
			||||||
@ -41,7 +47,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	insert := func(i, aid int) {
 | 
						insert := func(i, aid int) {
 | 
				
			||||||
		do(aid, func(c state.Cursor) {
 | 
							do(aid, func(c state.Cursor) {
 | 
				
			||||||
			if err := c.Save(&tc[i]); err != nil {
 | 
								if err := c.Save(&tc[i].state, &tc[i].ct); err != nil {
 | 
				
			||||||
				t.Fatalf("Save(&tc[%v]): error = %v", i, err)
 | 
									t.Fatalf("Save(&tc[%v]): error = %v", i, err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@ -51,15 +57,17 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
		do(aid, func(c state.Cursor) {
 | 
							do(aid, func(c state.Cursor) {
 | 
				
			||||||
			if entries, err := c.Load(); err != nil {
 | 
								if entries, err := c.Load(); err != nil {
 | 
				
			||||||
				t.Fatalf("Load: error = %v", err)
 | 
									t.Fatalf("Load: error = %v", err)
 | 
				
			||||||
			} else if got, ok := entries[tc[i].ID]; !ok {
 | 
								} else if got, ok := entries[tc[i].state.ID]; !ok {
 | 
				
			||||||
				t.Fatalf("Load: entry %s missing",
 | 
									t.Fatalf("Load: entry %s missing",
 | 
				
			||||||
					&tc[i].ID)
 | 
										&tc[i].state.ID)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				got.Time = tc[i].Time
 | 
									got.Time = tc[i].state.Time
 | 
				
			||||||
				if !reflect.DeepEqual(got, &tc[i]) {
 | 
									tc[i].state.Config = fst.Template()
 | 
				
			||||||
 | 
									if !reflect.DeepEqual(got, &tc[i].state) {
 | 
				
			||||||
					t.Fatalf("Load: entry %s got %#v, want %#v",
 | 
										t.Fatalf("Load: entry %s got %#v, want %#v",
 | 
				
			||||||
						&tc[i].ID, got, &tc[i])
 | 
											&tc[i].state.ID, got, &tc[i].state)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									tc[i].state.Config = nil
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -104,7 +112,7 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	t.Run("clear aid 1", func(t *testing.T) {
 | 
						t.Run("clear aid 1", func(t *testing.T) {
 | 
				
			||||||
		do(1, func(c state.Cursor) {
 | 
							do(1, func(c state.Cursor) {
 | 
				
			||||||
			if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
 | 
								if err := c.Destroy(tc[insertEntryOtherApp].state.ID); err != nil {
 | 
				
			||||||
				t.Fatalf("Destroy: error = %v", err)
 | 
									t.Fatalf("Destroy: error = %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@ -124,11 +132,13 @@ func testStore(t *testing.T, s state.Store) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func makeState(t *testing.T, s *state.State) {
 | 
					func makeState(t *testing.T, s *state.State, ct io.Writer) {
 | 
				
			||||||
	if err := fst.NewAppID(&s.ID); err != nil {
 | 
						if err := fst.NewAppID(&s.ID); err != nil {
 | 
				
			||||||
		t.Fatalf("cannot create dummy state: %v", err)
 | 
							t.Fatalf("cannot create dummy state: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	s.Config = fst.Template()
 | 
						if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("cannot encode dummy config: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	s.PID = rand.Int()
 | 
						s.PID = rand.Int()
 | 
				
			||||||
	s.Time = time.Now()
 | 
						s.Time = time.Now()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -93,13 +93,13 @@ func (d *DBus) apply(_ *I) error {
 | 
				
			|||||||
	ready := make(chan error, 1)
 | 
						ready := make(chan error, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// background dbus proxy start
 | 
						// background dbus proxy start
 | 
				
			||||||
	if err := d.proxy.Start(ready, d.out, true); err != nil {
 | 
						if err := d.proxy.Start(ready, d.out, true, true); err != nil {
 | 
				
			||||||
		return fmsg.WrapErrorSuffix(err,
 | 
							return fmsg.WrapErrorSuffix(err,
 | 
				
			||||||
			"cannot start message bus proxy:")
 | 
								"cannot start message bus proxy:")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmsg.VPrintln("starting message bus proxy:", d.proxy)
 | 
						fmsg.VPrintln("starting message bus proxy:", d.proxy)
 | 
				
			||||||
	if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
 | 
						if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
 | 
				
			||||||
		fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
 | 
							fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.BwrapStatic())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// background wait for proxy instance and notify completion
 | 
						// background wait for proxy instance and notify completion
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								ldd/exec.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								ldd/exec.go
									
									
									
									
									
								
							@ -16,13 +16,17 @@ func Exec(p string) ([]*Entry, error) {
 | 
				
			|||||||
		cmd *exec.Cmd
 | 
							cmd *exec.Cmd
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if b, err := helper.NewBwrap((&bwrap.Config{
 | 
						if b, err := helper.NewBwrap(
 | 
				
			||||||
 | 
							(&bwrap.Config{
 | 
				
			||||||
			Hostname:      "fortify-ldd",
 | 
								Hostname:      "fortify-ldd",
 | 
				
			||||||
			Chdir:         "/",
 | 
								Chdir:         "/",
 | 
				
			||||||
 | 
								Syscall:       &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
 | 
				
			||||||
			NewSession:    true,
 | 
								NewSession:    true,
 | 
				
			||||||
			DieWithParent: true,
 | 
								DieWithParent: true,
 | 
				
			||||||
	}).Bind("/", "/").DevTmpfs("/dev"),
 | 
							}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
 | 
				
			||||||
		nil, "ldd", func(_, _ int) []string { return []string{p} }); err != nil {
 | 
							nil, func(_, _ int) []string { return []string{p} },
 | 
				
			||||||
 | 
							nil, nil,
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		cmd = b.Unwrap()
 | 
							cmd = b.Unwrap()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								main.go
									
									
									
									
									
								
							@ -16,6 +16,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/dbus"
 | 
						"git.gensokyo.uk/security/fortify/dbus"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/fst"
 | 
						"git.gensokyo.uk/security/fortify/fst"
 | 
				
			||||||
 | 
						"git.gensokyo.uk/security/fortify/helper/bwrap"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal"
 | 
						"git.gensokyo.uk/security/fortify/internal"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/app"
 | 
						"git.gensokyo.uk/security/fortify/internal/app"
 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
@ -308,6 +309,10 @@ func runApp(config *fst.Config) {
 | 
				
			|||||||
	rs := new(app.RunState)
 | 
						rs := new(app.RunState)
 | 
				
			||||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
						ctx, cancel := context.WithCancel(context.Background())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if fmsg.Verbose() {
 | 
				
			||||||
 | 
							bwrap.CPrintln = fmsg.Println
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// handle signals for graceful shutdown
 | 
						// handle signals for graceful shutdown
 | 
				
			||||||
	sig := make(chan os.Signal, 2)
 | 
						sig := make(chan os.Signal, 2)
 | 
				
			||||||
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
						signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										88
									
								
								nixos.nix
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								nixos.nix
									
									
									
									
									
								
							@ -7,6 +7,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
let
 | 
					let
 | 
				
			||||||
  inherit (lib)
 | 
					  inherit (lib)
 | 
				
			||||||
 | 
					    mkMerge
 | 
				
			||||||
    mkIf
 | 
					    mkIf
 | 
				
			||||||
    mkDefault
 | 
					    mkDefault
 | 
				
			||||||
    mapAttrs
 | 
					    mapAttrs
 | 
				
			||||||
@ -19,6 +20,10 @@ let
 | 
				
			|||||||
    ;
 | 
					    ;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cfg = config.environment.fortify;
 | 
					  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
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -33,8 +38,7 @@ in
 | 
				
			|||||||
      group = "root";
 | 
					      group = "root";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    environment.etc = {
 | 
					    environment.etc.fsurc = {
 | 
				
			||||||
      fsurc = {
 | 
					 | 
				
			||||||
      mode = "0400";
 | 
					      mode = "0400";
 | 
				
			||||||
      text = foldlAttrs (
 | 
					      text = foldlAttrs (
 | 
				
			||||||
        acc: username: fid:
 | 
					        acc: username: fid:
 | 
				
			||||||
@ -42,16 +46,6 @@ in
 | 
				
			|||||||
      ) "" cfg.users;
 | 
					      ) "" 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" ];
 | 
					    systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    services.userdbd.enable = mkDefault true;
 | 
					    services.userdbd.enable = mkDefault true;
 | 
				
			||||||
@ -114,8 +108,8 @@ in
 | 
				
			|||||||
                    confinement = {
 | 
					                    confinement = {
 | 
				
			||||||
                      app_id = aid;
 | 
					                      app_id = aid;
 | 
				
			||||||
                      inherit (app) groups;
 | 
					                      inherit (app) groups;
 | 
				
			||||||
                      username = "u${toString fid}_a${toString aid}";
 | 
					                      username = getsubname fid aid;
 | 
				
			||||||
                      home = "${cfg.stateDir}/u${toString fid}/a${toString aid}";
 | 
					                      home = getsubhome fid aid;
 | 
				
			||||||
                      sandbox = {
 | 
					                      sandbox = {
 | 
				
			||||||
                        inherit (app)
 | 
					                        inherit (app)
 | 
				
			||||||
                          userns
 | 
					                          userns
 | 
				
			||||||
@ -123,6 +117,9 @@ in
 | 
				
			|||||||
                          dev
 | 
					                          dev
 | 
				
			||||||
                          env
 | 
					                          env
 | 
				
			||||||
                          ;
 | 
					                          ;
 | 
				
			||||||
 | 
					                        syscall = {
 | 
				
			||||||
 | 
					                          inherit (app) devel multiarch bluetooth;
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
                        map_real_uid = app.mapRealUid;
 | 
					                        map_real_uid = app.mapRealUid;
 | 
				
			||||||
                        no_new_session = app.tty;
 | 
					                        no_new_session = app.tty;
 | 
				
			||||||
                        filesystem =
 | 
					                        filesystem =
 | 
				
			||||||
@ -173,7 +170,9 @@ in
 | 
				
			|||||||
                  };
 | 
					                  };
 | 
				
			||||||
                in
 | 
					                in
 | 
				
			||||||
                pkgs.writeShellScriptBin app.name ''
 | 
					                pkgs.writeShellScriptBin app.name ''
 | 
				
			||||||
                  exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
					                  exec fortify${
 | 
				
			||||||
 | 
					                    if app.verbose then " -v" else ""
 | 
				
			||||||
 | 
					                  } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
 | 
				
			||||||
                ''
 | 
					                ''
 | 
				
			||||||
              ) cfg.apps;
 | 
					              ) cfg.apps;
 | 
				
			||||||
            in
 | 
					            in
 | 
				
			||||||
@ -205,16 +204,63 @@ in
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        users = foldlAttrs (
 | 
					        users = foldlAttrs (
 | 
				
			||||||
          acc: _: fid:
 | 
					          acc: _: fid:
 | 
				
			||||||
          mergeAttrsList (
 | 
					          mkMerge [
 | 
				
			||||||
 | 
					            (mergeAttrsList (
 | 
				
			||||||
              # aid 0 is reserved
 | 
					              # aid 0 is reserved
 | 
				
			||||||
              imap1 (aid: app: {
 | 
					              imap1 (aid: app: {
 | 
				
			||||||
              "u${toString fid}_a${toString aid}" = app.extraConfig // {
 | 
					                ${getsubname fid aid} = mkMerge [
 | 
				
			||||||
                home.packages = app.packages;
 | 
					                  (cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
 | 
				
			||||||
              };
 | 
					                  app.extraConfig
 | 
				
			||||||
 | 
					                  { home.packages = app.packages; }
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
              }) cfg.apps
 | 
					              }) cfg.apps
 | 
				
			||||||
          )
 | 
					            ))
 | 
				
			||||||
          // acc
 | 
					            { ${getsubname fid 0} = cfg.home-manager (getsubname fid 0) (getsubuid fid 0); }
 | 
				
			||||||
 | 
					            acc
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
        ) privPackages cfg.users;
 | 
					        ) 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:*
 | 
					*Default:*
 | 
				
			||||||
` <derivation fortify-0.2.10> `
 | 
					` <derivation fortify-0.2.11> `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -77,6 +77,30 @@ list of package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## environment\.fortify\.apps\.\*\.bluetooth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Whether to enable AF_BLUETOOTH socket operations\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Type:*
 | 
				
			||||||
 | 
					boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Default:*
 | 
				
			||||||
 | 
					` false `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Example:*
 | 
				
			||||||
 | 
					` true `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## environment\.fortify\.apps\.\*\.capability\.dbus
 | 
					## environment\.fortify\.apps\.\*\.capability\.dbus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -218,7 +242,31 @@ null or anything
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable access to all devices within the sandbox\.
 | 
					Whether to enable access to all devices\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Type:*
 | 
				
			||||||
 | 
					boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Default:*
 | 
				
			||||||
 | 
					` false `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Example:*
 | 
				
			||||||
 | 
					` true `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## environment\.fortify\.apps\.\*\.devel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Whether to enable development kernel APIs\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -357,7 +405,31 @@ null or string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable mapping to fortify’s real UID within the sandbox\.
 | 
					Whether to enable mapping to priv-user uid\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Type:*
 | 
				
			||||||
 | 
					boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Default:*
 | 
				
			||||||
 | 
					` false `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Example:*
 | 
				
			||||||
 | 
					` true `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## environment\.fortify\.apps\.\*\.multiarch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Whether to enable multiarch kernel support\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -395,7 +467,7 @@ string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable network access within the sandbox\.
 | 
					Whether to enable network access\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -419,7 +491,7 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable nix daemon access within the sandbox\.
 | 
					Whether to enable nix daemon\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -482,7 +554,7 @@ null or package
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable allow access to the controlling terminal\.
 | 
					Whether to enable access to the controlling terminal\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -506,7 +578,7 @@ boolean
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Whether to enable userns within the sandbox\.
 | 
					Whether to enable user namespace\.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -526,6 +598,44 @@ 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
 | 
					## environment\.fortify\.stateDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										29
									
								
								options.nix
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								options.nix
									
									
									
									
									
								
							@ -26,6 +26,17 @@ 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 {
 | 
					      apps = mkOption {
 | 
				
			||||||
        type =
 | 
					        type =
 | 
				
			||||||
          let
 | 
					          let
 | 
				
			||||||
@ -50,6 +61,8 @@ in
 | 
				
			|||||||
                '';
 | 
					                '';
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              verbose = mkEnableOption "launchers with verbose output";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              id = mkOption {
 | 
					              id = mkOption {
 | 
				
			||||||
                type = nullOr str;
 | 
					                type = nullOr str;
 | 
				
			||||||
                default = null;
 | 
					                default = null;
 | 
				
			||||||
@ -128,16 +141,20 @@ in
 | 
				
			|||||||
                '';
 | 
					                '';
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              nix = mkEnableOption "nix daemon access within the sandbox";
 | 
					              nix = mkEnableOption "nix daemon";
 | 
				
			||||||
              userns = mkEnableOption "userns within the sandbox";
 | 
					              userns = mkEnableOption "user namespace";
 | 
				
			||||||
              mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
 | 
					              mapRealUid = mkEnableOption "mapping to priv-user uid";
 | 
				
			||||||
              dev = mkEnableOption "access to all devices within the sandbox";
 | 
					              dev = mkEnableOption "access to all devices";
 | 
				
			||||||
              tty = mkEnableOption "allow access to the controlling terminal";
 | 
					              tty = mkEnableOption "access to the controlling terminal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              net = mkEnableOption "network access within the sandbox" // {
 | 
					              net = mkEnableOption "network access" // {
 | 
				
			||||||
                default = true;
 | 
					                default = true;
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              devel = mkEnableOption "development kernel APIs";
 | 
				
			||||||
 | 
					              multiarch = mkEnableOption "multiarch kernel support";
 | 
				
			||||||
 | 
					              bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              gpu = mkOption {
 | 
					              gpu = mkOption {
 | 
				
			||||||
                type = nullOr bool;
 | 
					                type = nullOr bool;
 | 
				
			||||||
                default = null;
 | 
					                default = null;
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@
 | 
				
			|||||||
  bubblewrap,
 | 
					  bubblewrap,
 | 
				
			||||||
  pkg-config,
 | 
					  pkg-config,
 | 
				
			||||||
  libffi,
 | 
					  libffi,
 | 
				
			||||||
 | 
					  libseccomp,
 | 
				
			||||||
  acl,
 | 
					  acl,
 | 
				
			||||||
  wayland,
 | 
					  wayland,
 | 
				
			||||||
  wayland-protocols,
 | 
					  wayland-protocols,
 | 
				
			||||||
@ -15,7 +16,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
buildGoModule rec {
 | 
					buildGoModule rec {
 | 
				
			||||||
  pname = "fortify";
 | 
					  pname = "fortify";
 | 
				
			||||||
  version = "0.2.10";
 | 
					  version = "0.2.11";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  src = builtins.path {
 | 
					  src = builtins.path {
 | 
				
			||||||
    name = "fortify-src";
 | 
					    name = "fortify-src";
 | 
				
			||||||
@ -45,6 +46,7 @@ buildGoModule rec {
 | 
				
			|||||||
  buildInputs =
 | 
					  buildInputs =
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
      libffi
 | 
					      libffi
 | 
				
			||||||
 | 
					      libseccomp
 | 
				
			||||||
      acl
 | 
					      acl
 | 
				
			||||||
      wayland
 | 
					      wayland
 | 
				
			||||||
      wayland-protocols
 | 
					      wayland-protocols
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								print.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								print.go
									
									
									
									
									
								
							@ -53,6 +53,10 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
 | 
				
			|||||||
	now := time.Now().UTC()
 | 
						now := time.Now().UTC()
 | 
				
			||||||
	w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
 | 
						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 {
 | 
						if instance != nil {
 | 
				
			||||||
		fmt.Fprintf(w, "State\n")
 | 
							fmt.Fprintf(w, "State\n")
 | 
				
			||||||
		fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
 | 
							fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
 | 
				
			||||||
@ -106,9 +110,6 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Env           map[string]string   `json:"env"`
 | 
							// Env           map[string]string   `json:"env"`
 | 
				
			||||||
		// Link          [][2]string         `json:"symlink"`
 | 
							// 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, " Command:\t%s\n", strings.Join(config.Command, " "))
 | 
				
			||||||
	fmt.Fprintf(w, "\n")
 | 
						fmt.Fprintf(w, "\n")
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										98
									
								
								test.nix
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								test.nix
									
									
									
									
									
								
							@ -44,7 +44,6 @@ nixosTest {
 | 
				
			|||||||
          # For glinfo and wayland-info:
 | 
					          # For glinfo and wayland-info:
 | 
				
			||||||
          mesa-demos
 | 
					          mesa-demos
 | 
				
			||||||
          wayland-utils
 | 
					          wayland-utils
 | 
				
			||||||
          alacritty
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          # For D-Bus tests:
 | 
					          # For D-Bus tests:
 | 
				
			||||||
          libnotify
 | 
					          libnotify
 | 
				
			||||||
@ -83,7 +82,7 @@ nixosTest {
 | 
				
			|||||||
          sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
 | 
					          sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          sway --validate
 | 
					          sway --validate
 | 
				
			||||||
          sway && touch /tmp/sway-exit-ok
 | 
					          systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
 | 
				
			||||||
        fi
 | 
					        fi
 | 
				
			||||||
      '';
 | 
					      '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -111,6 +110,43 @@ nixosTest {
 | 
				
			|||||||
        enable = true;
 | 
					        enable = true;
 | 
				
			||||||
        stateDir = "/var/lib/fortify";
 | 
					        stateDir = "/var/lib/fortify";
 | 
				
			||||||
        users.alice = 0;
 | 
					        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 = [
 | 
					      imports = [
 | 
				
			||||||
@ -176,16 +212,18 @@ nixosTest {
 | 
				
			|||||||
        machine.screenshot(name)
 | 
					        machine.screenshot(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_state(command, enablements):
 | 
					    def check_state(name, enablements):
 | 
				
			||||||
        instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
 | 
					        instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
 | 
				
			||||||
        if len(instances) != 1:
 | 
					        if len(instances) != 1:
 | 
				
			||||||
            raise Exception(f"unexpected state length {len(instances)}")
 | 
					            raise Exception(f"unexpected state length {len(instances)}")
 | 
				
			||||||
        instance = next(iter(instances.values()))
 | 
					        instance = next(iter(instances.values()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if instance['config']['command'] != command:
 | 
					        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")):
 | 
				
			||||||
            raise Exception(f"unexpected command {instance['config']['command']}")
 | 
					            raise Exception(f"unexpected command {instance['config']['command']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if instance['config']['confinement']['enablements'] != enablements:
 | 
					        if config['confinement']['enablements'] != enablements:
 | 
				
			||||||
            raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
					            raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -212,60 +250,60 @@ nixosTest {
 | 
				
			|||||||
    # Create fortify uid 0 state directory:
 | 
					    # Create fortify uid 0 state directory:
 | 
				
			||||||
    machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
 | 
					    machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Start fortify outside Wayland session:
 | 
					    # Start fortify permissive defaults outside Wayland session:
 | 
				
			||||||
    print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
					    print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
				
			||||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Start fortify within Wayland session:
 | 
					    # Start fortify permissive defaults within Wayland session:
 | 
				
			||||||
    fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
					    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")
 | 
					    machine.wait_for_file("/tmp/dbus-done")
 | 
				
			||||||
    collect_state_ui("dbus_notify_exited")
 | 
					    collect_state_ui("dbus_notify_exited")
 | 
				
			||||||
    machine.succeed("pkill -9 mako")
 | 
					    machine.succeed("pkill -9 mako")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Start a terminal (foot) within fortify:
 | 
					    # Start app (foot) with Wayland enablement:
 | 
				
			||||||
    fortify("run --wayland foot")
 | 
					    swaymsg("exec ne-foot")
 | 
				
			||||||
    wait_for_window("u0_a0@machine")
 | 
					    wait_for_window("u0_a1@machine")
 | 
				
			||||||
    machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
					    machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
				
			||||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
 | 
				
			||||||
    collect_state_ui("foot_wayland_permissive")
 | 
					    collect_state_ui("foot_wayland")
 | 
				
			||||||
    check_state(["foot"], 1)
 | 
					    check_state("ne-foot", 1)
 | 
				
			||||||
    # Verify acl on XDG_RUNTIME_DIR:
 | 
					    # Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000"))
 | 
					    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
 | 
				
			||||||
    machine.send_chars("exit\n")
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
    machine.wait_until_fails("pgrep foot")
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
    # Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					    # Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
 | 
					    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Start a terminal (foot) within fortify from a terminal:
 | 
					    # Start app (foot) with Wayland enablement 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'")
 | 
					    swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
				
			||||||
    wait_for_window("u0_a0@machine")
 | 
					    wait_for_window("u0_a1@machine")
 | 
				
			||||||
    machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
					    machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
				
			||||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term")
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
 | 
				
			||||||
    machine.wait_for_file("/tmp/ps-show-ok")
 | 
					    machine.wait_for_file("/tmp/ps-show-ok")
 | 
				
			||||||
    collect_state_ui("foot_wayland_permissive_term")
 | 
					    collect_state_ui("foot_wayland_term")
 | 
				
			||||||
    check_state(["foot"], 1)
 | 
					    check_state("ne-foot", 1)
 | 
				
			||||||
    machine.send_chars("exit\n")
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
    wait_for_window("foot")
 | 
					    wait_for_window("foot")
 | 
				
			||||||
    machine.send_key("ctrl-c")
 | 
					    machine.send_key("ctrl-c")
 | 
				
			||||||
    machine.wait_until_fails("pgrep foot")
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test PulseAudio (fortify does not support PipeWire yet):
 | 
					    # Test PulseAudio (fortify does not support PipeWire yet):
 | 
				
			||||||
    fortify("run --wayland --pulse foot")
 | 
					    swaymsg("exec pa-foot")
 | 
				
			||||||
    wait_for_window("u0_a0@machine")
 | 
					    wait_for_window("u0_a2@machine")
 | 
				
			||||||
    machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
					    machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
				
			||||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-pulse")
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
 | 
				
			||||||
    collect_state_ui("pulse_wayland")
 | 
					    collect_state_ui("pulse_wayland")
 | 
				
			||||||
    check_state(["foot"], 9)
 | 
					    check_state("pa-foot", 9)
 | 
				
			||||||
    machine.send_chars("exit\n")
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
    machine.wait_until_fails("pgrep foot")
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test XWayland (foot does not support X):
 | 
					    # Test XWayland (foot does not support X):
 | 
				
			||||||
    fortify("run -X alacritty")
 | 
					    swaymsg("exec x11-alacritty")
 | 
				
			||||||
    wait_for_window("u0_a0@machine")
 | 
					    wait_for_window("u0_a3@machine")
 | 
				
			||||||
    machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
					    machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
				
			||||||
    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11")
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
 | 
				
			||||||
    collect_state_ui("alacritty_x11_permissive")
 | 
					    collect_state_ui("alacritty_x11")
 | 
				
			||||||
    check_state(["alacritty"], 2)
 | 
					    check_state("x11-alacritty", 2)
 | 
				
			||||||
    machine.send_chars("exit\n")
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
    machine.wait_until_fails("pgrep alacritty")
 | 
					    machine.wait_until_fails("pgrep alacritty")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user