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]
|
||||
Enablements system.Enablements `json:"enablements"`
|
||||
|
||||
// passed through inverted to [bwrap.SyscallPolicy]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [bwrap.SyscallPolicy]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
GPU bool `json:"gpu"`
|
||||
// store path to nixGL mesa wrappers
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
@ -96,6 +97,7 @@ func actionStart(args []string) {
|
||||
UserNS: app.UserNS,
|
||||
Net: app.Net,
|
||||
Dev: app.Dev,
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
|
||||
NoNewSession: app.NoNewSession || dropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
DirectWayland: app.DirectWayland,
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
@ -34,6 +35,7 @@ func withNixDaemon(
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
UserNS: true, // nix sandbox requires userns
|
||||
Net: net,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
|
||||
@ -65,6 +67,7 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
|
||||
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
|
||||
Sandbox: &fst.SandboxConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
|
||||
NoNewSession: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
|
||||
|
@ -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) {
|
||||
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",
|
||||
err, errors.New(want))
|
||||
return
|
||||
@ -175,7 +175,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
||||
}
|
||||
|
||||
t.Run("sealed start of "+id, func(t *testing.T) {
|
||||
if err := p.Start(nil, output, sandbox); err != nil {
|
||||
if err := p.Start(nil, output, sandbox, false); err != nil {
|
||||
t.Fatalf("Start(nil, nil) error = %v",
|
||||
err)
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ func (p *Proxy) String() string {
|
||||
return "(unsealed dbus proxy)"
|
||||
}
|
||||
|
||||
func (p *Proxy) Bwrap() []string {
|
||||
func (p *Proxy) BwrapStatic() []string {
|
||||
return p.bwrap.Args()
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
// Start launches the D-Bus proxy and sets up the Wait method.
|
||||
// ready should be buffered and must only be received from once.
|
||||
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
|
||||
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool) error {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
@ -67,11 +67,16 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
|
||||
Unshare: nil,
|
||||
Hostname: "fortify-dbus",
|
||||
Chdir: "/",
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||
Clearenv: true,
|
||||
NewSession: true,
|
||||
DieWithParent: true,
|
||||
}
|
||||
|
||||
if !seccomp {
|
||||
bc.Syscall = nil
|
||||
}
|
||||
|
||||
// resolve proxy socket directories
|
||||
bindTarget := make(map[string]struct{}, 2)
|
||||
for _, ps := range []string{p.session[1], p.system[1]} {
|
||||
@ -110,7 +115,7 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
|
||||
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()
|
||||
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/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
|
||||
|
||||
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
|
||||
|
||||
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
|
||||
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
|
||||
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
|
||||
|
@ -132,6 +132,7 @@
|
||||
[
|
||||
musl
|
||||
libffi
|
||||
libseccomp
|
||||
acl
|
||||
wayland
|
||||
wayland-protocols
|
||||
@ -172,6 +173,7 @@
|
||||
[
|
||||
musl
|
||||
libffi
|
||||
libseccomp
|
||||
acl
|
||||
wayland
|
||||
wayland-protocols
|
||||
|
@ -2,12 +2,13 @@ package fst
|
||||
|
||||
import (
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/system"
|
||||
)
|
||||
|
||||
const Tmp = "/.fortify"
|
||||
|
||||
// Config is used to seal an *App
|
||||
// Config is used to seal an app
|
||||
type Config struct {
|
||||
// application ID
|
||||
ID string `json:"id"`
|
||||
@ -107,9 +108,10 @@ func Template() *Config {
|
||||
Hostname: "localhost",
|
||||
UserNS: true,
|
||||
Net: true,
|
||||
Dev: true,
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||
NoNewSession: true,
|
||||
MapRealUID: true,
|
||||
Dev: true,
|
||||
DirectWayland: false,
|
||||
// example API credentials pulled from Google Chrome
|
||||
// DO NOT USE THESE IN A REAL BROWSER
|
||||
@ -123,7 +125,8 @@ func Template() *Config {
|
||||
{Src: "/run/current-system"},
|
||||
{Src: "/run/opengl-driver"},
|
||||
{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},
|
||||
},
|
||||
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
|
||||
@ -131,6 +134,10 @@ func Template() *Config {
|
||||
AutoEtc: true,
|
||||
Override: []string{"/var/run/nscd"},
|
||||
},
|
||||
ExtraPerms: []*ExtraPermConfig{
|
||||
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
|
||||
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
|
||||
},
|
||||
SystemBus: &dbus.Config{
|
||||
See: nil,
|
||||
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
||||
|
@ -22,6 +22,8 @@ type SandboxConfig struct {
|
||||
Net bool `json:"net,omitempty"`
|
||||
// share all devices
|
||||
Dev bool `json:"dev,omitempty"`
|
||||
// seccomp syscall filter policy
|
||||
Syscall *bwrap.SyscallPolicy `json:"syscall"`
|
||||
// do not run in new session
|
||||
NoNewSession bool `json:"no_new_session,omitempty"`
|
||||
// map target user uid to privileged user uid in the user namespace
|
||||
@ -50,6 +52,10 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
|
||||
return nil, errors.New("nil sandbox config")
|
||||
}
|
||||
|
||||
if s.Syscall == nil {
|
||||
fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
|
||||
}
|
||||
|
||||
var uid int
|
||||
if !s.MapRealUID {
|
||||
uid = 65534
|
||||
@ -69,6 +75,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
|
||||
so this capacity should eliminate copies for most setups */
|
||||
Filesystem: make([]bwrap.FSBuilder, 0, 256),
|
||||
|
||||
Syscall: s.Syscall,
|
||||
NewSession: !s.NoNewSession,
|
||||
DieWithParent: true,
|
||||
AsInit: true,
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/proc"
|
||||
)
|
||||
|
||||
// BubblewrapName is the file name or path to bubblewrap.
|
||||
@ -21,8 +20,6 @@ type bubblewrap struct {
|
||||
|
||||
// bwrap pipes
|
||||
control *pipes
|
||||
// sync pipe
|
||||
sync *os.File
|
||||
// returns an array of arguments passed directly
|
||||
// to the child process spawned by bwrap
|
||||
argF func(argsFD, statFD int) []string
|
||||
@ -49,11 +46,6 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
|
||||
return errors.New("exec: already started")
|
||||
}
|
||||
|
||||
// pass sync fd to bwrap
|
||||
if b.sync != nil {
|
||||
b.Cmd.Args = append(b.Cmd.Args, "--sync-fd", strconv.Itoa(int(proc.ExtraFile(b.Cmd, b.sync))))
|
||||
}
|
||||
|
||||
// prepare bwrap pipe and args
|
||||
if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
|
||||
return err
|
||||
@ -119,8 +111,13 @@ func (b *bubblewrap) Unwrap() *exec.Cmd {
|
||||
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
|
||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper {
|
||||
b, err := NewBwrap(conf, wt, name, argF)
|
||||
func MustNewBwrap(
|
||||
conf *bwrap.Config, name string,
|
||||
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
||||
extraFiles []*os.File,
|
||||
syncFd *os.File,
|
||||
) Helper {
|
||||
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
} 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.
|
||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
func NewBwrap(conf *bwrap.Config, 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)
|
||||
|
||||
if args, err := NewCheckedArgs(conf.Args()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
b.control = &pipes{args: args}
|
||||
}
|
||||
|
||||
b.sync = conf.Sync()
|
||||
b.argF = argF
|
||||
b.name = name
|
||||
if wt != nil {
|
||||
b.controlPt = &pipes{args: wt}
|
||||
}
|
||||
|
||||
b.Cmd = execCommand(BubblewrapName)
|
||||
b.control = new(pipes)
|
||||
args := conf.Args()
|
||||
if fdArgs, err := conf.FDArgs(syncFd, &extraFiles); err != nil {
|
||||
return nil, err
|
||||
} else if b.control.args, err = NewCheckedArgs(append(args, fdArgs...)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
b.Cmd.ExtraFiles = extraFiles
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
package bwrap
|
||||
|
||||
import "encoding/gob"
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/proc"
|
||||
)
|
||||
|
||||
type Builder interface {
|
||||
Len() int
|
||||
@ -12,6 +19,11 @@ type FSBuilder interface {
|
||||
Builder
|
||||
}
|
||||
|
||||
type FDBuilder interface {
|
||||
Len() int
|
||||
Append(args *[]string, extraFiles *[]*os.File) error
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(new(pairF))
|
||||
gob.Register(new(stringF))
|
||||
@ -45,6 +57,33 @@ func (s stringF) Append(args *[]string) {
|
||||
*args = append(*args, s[0], s[1])
|
||||
}
|
||||
|
||||
type fileF struct {
|
||||
name string
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (f *fileF) Len() int {
|
||||
if f.file == nil {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func (f *fileF) Append(args *[]string, extraFiles *[]*os.File) error {
|
||||
if f.file == nil {
|
||||
return nil
|
||||
}
|
||||
extraFile(args, extraFiles, f.name, f.file)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extraFile(args *[]string, extraFiles *[]*os.File, name string, f *os.File) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
*args = append(*args, name, strconv.Itoa(int(proc.ExtraFileSlice(extraFiles, f))))
|
||||
}
|
||||
|
||||
// Args returns a slice of bwrap args corresponding to c.
|
||||
func (c *Config) Args() (args []string) {
|
||||
builders := []Builder{
|
||||
@ -75,3 +114,25 @@ func (c *Config) Args() (args []string) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) FDArgs(syncFd *os.File, extraFiles *[]*os.File) (args []string, err error) {
|
||||
builders := []FDBuilder{
|
||||
&seccompBuilder{c},
|
||||
&fileF{positionalArgs[SyncFd], syncFd},
|
||||
}
|
||||
|
||||
argc := 0
|
||||
for _, b := range builders {
|
||||
argc += b.Len()
|
||||
}
|
||||
|
||||
args = make([]string, 0, argc)
|
||||
*extraFiles = slices.Grow(*extraFiles, len(builders))
|
||||
|
||||
for _, b := range builders {
|
||||
if err = b.Append(&args, extraFiles); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -161,10 +161,3 @@ func (c *Config) SetGID(gid int) *Config {
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// SetSync sets the sync pipe kept open while sandbox is running
|
||||
// (--sync-fd FD)
|
||||
func (c *Config) SetSync(s *os.File) *Config {
|
||||
c.sync = s
|
||||
return c
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
package bwrap
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// unshare every namespace we support by default if nil
|
||||
// (--unshare-all)
|
||||
@ -51,6 +47,10 @@ type Config struct {
|
||||
// (--chmod OCTAL PATH)
|
||||
Chmod ChmodConfig `json:"chmod,omitempty"`
|
||||
|
||||
// load and use seccomp rules from FD (not repeatable)
|
||||
// (--seccomp FD)
|
||||
Syscall *SyscallPolicy
|
||||
|
||||
// create a new terminal session
|
||||
// (--new-session)
|
||||
NewSession bool `json:"new_session"`
|
||||
@ -61,10 +61,6 @@ type Config struct {
|
||||
// (--as-pid-1)
|
||||
AsInit bool `json:"as_init"`
|
||||
|
||||
// keep this fd open while sandbox is running
|
||||
// (--sync-fd FD)
|
||||
sync *os.File
|
||||
|
||||
/* unmapped options include:
|
||||
--unshare-user-try Create new user namespace if possible else continue by skipping it
|
||||
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
|
||||
@ -78,7 +74,6 @@ type Config struct {
|
||||
--file FD DEST Copy from FD to destination DEST
|
||||
--bind-data FD DEST Copy from FD to file which is bind-mounted on DEST
|
||||
--ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST
|
||||
--seccomp FD Load and use seccomp rules from FD (not repeatable)
|
||||
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
|
||||
--block-fd FD Block on FD until some data to read is available
|
||||
--userns-block-fd FD Block on FD until the user namespace is ready
|
||||
@ -90,12 +85,6 @@ type Config struct {
|
||||
among which --args is used internally for passing arguments */
|
||||
}
|
||||
|
||||
// Sync keep this fd open while sandbox is running
|
||||
// (--sync-fd FD)
|
||||
func (c *Config) Sync() *os.File {
|
||||
return c.sync
|
||||
}
|
||||
|
||||
type UnshareConfig struct {
|
||||
// (--unshare-user)
|
||||
// create new user namespace
|
||||
|
@ -126,8 +126,7 @@ func TestConfig_Args(t *testing.T) {
|
||||
name: "uid gid sync",
|
||||
conf: (new(bwrap.Config)).
|
||||
SetUID(1971).
|
||||
SetGID(100).
|
||||
SetSync(os.Stdin),
|
||||
SetGID(100),
|
||||
want: []string{
|
||||
"--unshare-all", "--unshare-user",
|
||||
"--disable-userns", "--assert-userns-disabled",
|
||||
@ -135,8 +134,6 @@ func TestConfig_Args(t *testing.T) {
|
||||
"--uid", "1971",
|
||||
// SetGID(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", "", "")
|
||||
})
|
||||
|
||||
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
|
||||
TmpOverlay
|
||||
ROOverlay
|
||||
|
||||
SyncFd
|
||||
Seccomp
|
||||
)
|
||||
|
||||
var positionalArgs = [...]string{
|
||||
@ -70,6 +73,9 @@ var positionalArgs = [...]string{
|
||||
Overlay: "--overlay",
|
||||
TmpOverlay: "--tmp-overlay",
|
||||
ROOverlay: "--ro-overlay",
|
||||
|
||||
SyncFd: "--sync-fd",
|
||||
Seccomp: "--seccomp",
|
||||
}
|
||||
|
||||
type PermConfig[T FSBuilder] struct {
|
||||
|
@ -31,7 +31,11 @@ func TestBwrap(t *testing.T) {
|
||||
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) {
|
||||
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) {
|
||||
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",
|
||||
sc, argsWt, "fortify")
|
||||
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) {
|
||||
@ -69,13 +81,21 @@ func TestBwrap(t *testing.T) {
|
||||
}()
|
||||
|
||||
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) {
|
||||
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()
|
||||
|
||||
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) {
|
||||
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 (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
||||
@ -30,9 +29,6 @@ type RunState struct {
|
||||
}
|
||||
|
||||
type app struct {
|
||||
// single-use config reference
|
||||
ct *appCt
|
||||
|
||||
// application unique identifier
|
||||
id *fst.ID
|
||||
// operating system interface
|
||||
@ -74,24 +70,3 @@ func New(os linux.System) (App, error) {
|
||||
a.os = os
|
||||
return a, fst.NewAppID(a.id)
|
||||
}
|
||||
|
||||
// appCt ensures its wrapped val is only accessed once
|
||||
type appCt struct {
|
||||
val *fst.Config
|
||||
done *atomic.Bool
|
||||
}
|
||||
|
||||
func (a *appCt) Unwrap() *fst.Config {
|
||||
if !a.done.Load() {
|
||||
defer a.done.Store(true)
|
||||
return a.val
|
||||
}
|
||||
panic("attempted to access config reference twice")
|
||||
}
|
||||
|
||||
func newAppCt(config *fst.Config) (ct *appCt) {
|
||||
ct = new(appCt)
|
||||
ct.done = new(atomic.Bool)
|
||||
ct.val = config
|
||||
return ct
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ var testCasesPd = []sealTestCase{
|
||||
Net: true,
|
||||
UserNS: true,
|
||||
Clearenv: true,
|
||||
Syscall: new(bwrap.SyscallPolicy),
|
||||
Chdir: "/home/chronos",
|
||||
SetEnv: map[string]string{
|
||||
"HOME": "/home/chronos",
|
||||
@ -258,6 +259,7 @@ var testCasesPd = []sealTestCase{
|
||||
UserNS: true,
|
||||
Chdir: "/home/chronos",
|
||||
Clearenv: true,
|
||||
Syscall: new(bwrap.SyscallPolicy),
|
||||
SetEnv: map[string]string{
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
|
||||
|
@ -1,8 +1,11 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"regexp"
|
||||
@ -11,6 +14,7 @@ import (
|
||||
"git.gensokyo.uk/security/fortify/acl"
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
@ -47,6 +51,8 @@ type appSeal struct {
|
||||
|
||||
// pass-through enablement tracking from config
|
||||
et system.Enablements
|
||||
// initial config gob encoding buffer
|
||||
ct io.WriterTo
|
||||
// wayland socket direct access
|
||||
directWayland bool
|
||||
// extra UpdatePerm ops
|
||||
@ -85,6 +91,14 @@ func (a *app) Seal(config *fst.Config) error {
|
||||
// create seal
|
||||
seal := new(appSeal)
|
||||
|
||||
// encode initial configuration for state tracking
|
||||
ct := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
"cannot encode initial config:")
|
||||
}
|
||||
seal.ct = ct
|
||||
|
||||
// fetch system constants
|
||||
seal.Paths = a.os.Paths()
|
||||
|
||||
@ -181,6 +195,7 @@ func (a *app) Seal(config *fst.Config) error {
|
||||
conf := &fst.SandboxConfig{
|
||||
UserNS: true,
|
||||
Net: true,
|
||||
Syscall: new(bwrap.SyscallPolicy),
|
||||
NoNewSession: true,
|
||||
AutoEtc: true,
|
||||
}
|
||||
@ -252,6 +267,5 @@ func (a *app) Seal(config *fst.Config) error {
|
||||
|
||||
// seal app and release lock
|
||||
a.seal = seal
|
||||
a.ct = newAppCt(config)
|
||||
return nil
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/acl"
|
||||
"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
|
||||
if src, err := discoverPulseCookie(os); err != nil {
|
||||
// not fatal
|
||||
fmsg.VPrintln(err.(*fmsg.BaseError).Message())
|
||||
fmsg.VPrintln(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
|
||||
} else {
|
||||
dst := path.Join(seal.share, "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
|
||||
if err := a.seal.sys.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.seal.sys.needRevert = true
|
||||
|
||||
// export sync pipe from sys
|
||||
a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
|
||||
|
||||
// start shim via manager
|
||||
a.shim = new(shim.Shim)
|
||||
waitErr := make(chan error, 1)
|
||||
if startTime, err := a.shim.Start(); 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
|
||||
} else {
|
||||
// shim process created
|
||||
@ -88,7 +75,14 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
|
||||
}()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -96,14 +90,13 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
|
||||
sd := state.State{
|
||||
ID: *a.id,
|
||||
PID: a.shim.Unwrap().Process.Pid,
|
||||
Config: a.ct.Unwrap(),
|
||||
Time: *startTime,
|
||||
}
|
||||
|
||||
// register process state
|
||||
var err0 = new(StateStoreError)
|
||||
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
|
||||
err0.InnerErr = c.Save(&sd)
|
||||
err0.InnerErr = c.Save(&sd, a.seal.ct)
|
||||
})
|
||||
a.seal.sys.saveState = true
|
||||
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) {
|
||||
return ExtraFileSlice(&cmd.ExtraFiles, f)
|
||||
}
|
||||
|
||||
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
|
||||
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
|
||||
fd = uintptr(3 + len(cmd.ExtraFiles))
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
|
||||
fd = uintptr(3 + len(*extraFiles))
|
||||
*extraFiles = append(*extraFiles, f)
|
||||
return
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/proc"
|
||||
@ -29,14 +29,6 @@ func Main() {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// re-exec
|
||||
if len(os.Args) > 0 && (os.Args[0] != "fortify" || os.Args[1] != "shim" || len(os.Args) != 2) && path.IsAbs(os.Args[0]) {
|
||||
if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil {
|
||||
fmsg.Println("cannot re-exec self:", err)
|
||||
// continue anyway
|
||||
}
|
||||
}
|
||||
|
||||
// receive setup payload
|
||||
var (
|
||||
payload Payload
|
||||
@ -62,8 +54,9 @@ func Main() {
|
||||
}
|
||||
|
||||
// restore bwrap sync fd
|
||||
var syncFd *os.File
|
||||
if payload.Sync != nil {
|
||||
payload.Bwrap.SetSync(os.NewFile(*payload.Sync, "sync"))
|
||||
syncFd = os.NewFile(*payload.Sync, "sync")
|
||||
}
|
||||
|
||||
// close setup socket
|
||||
@ -134,17 +127,19 @@ func Main() {
|
||||
conf.Symlink("fortify", innerInit)
|
||||
|
||||
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
|
||||
if b, err := helper.NewBwrap(conf, nil, innerInit,
|
||||
func(int, int) []string { return make([]string, 0) }); err != nil {
|
||||
if fmsg.Verbose() {
|
||||
bwrap.CPrintln = fmsg.Println
|
||||
}
|
||||
if b, err := helper.NewBwrap(
|
||||
conf, innerInit,
|
||||
nil, func(int, int) []string { return make([]string, 0) },
|
||||
extraFiles,
|
||||
syncFd,
|
||||
); err != nil {
|
||||
fmsg.Fatalf("malformed sandbox config: %v", err)
|
||||
} else {
|
||||
cmd := b.Unwrap()
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = extraFiles
|
||||
|
||||
if fmsg.Verbose() {
|
||||
fmsg.VPrintln("bwrap args:", conf.Args())
|
||||
}
|
||||
|
||||
// run and pass through exit code
|
||||
if err = b.Start(); err != nil {
|
||||
|
@ -20,22 +20,12 @@ import (
|
||||
type Shim struct {
|
||||
// user switcher process
|
||||
cmd *exec.Cmd
|
||||
// uid of shim target user
|
||||
uid uint32
|
||||
// string representation of application id
|
||||
aid string
|
||||
// string representation of supplementary group ids
|
||||
supp []string
|
||||
// fallback exit notifier with error returned killing the process
|
||||
killFallback chan error
|
||||
// shim setup payload
|
||||
payload *Payload
|
||||
// monitor to shim encoder
|
||||
encoder *gob.Encoder
|
||||
}
|
||||
|
||||
func New(uid uint32, aid string, supp []string, payload *Payload) *Shim {
|
||||
return &Shim{uid: uid, aid: aid, supp: supp, payload: payload}
|
||||
// bwrap --sync-fd value
|
||||
sync *uintptr
|
||||
}
|
||||
|
||||
func (s *Shim) String() string {
|
||||
@ -53,7 +43,14 @@ func (s *Shim) WaitFallback() chan error {
|
||||
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
|
||||
var fsu string
|
||||
if p, ok := internal.Path(internal.Fsu); !ok {
|
||||
@ -72,22 +69,22 @@ func (s *Shim) Start() (*time.Time, error) {
|
||||
s.encoder = e
|
||||
s.cmd.Env = []string{
|
||||
Env + "=" + strconv.Itoa(fd),
|
||||
"FORTIFY_APP_ID=" + s.aid,
|
||||
"FORTIFY_APP_ID=" + aid,
|
||||
}
|
||||
}
|
||||
|
||||
// format fsu supplementary groups
|
||||
if len(s.supp) > 0 {
|
||||
fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
|
||||
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
|
||||
if len(supp) > 0 {
|
||||
fmsg.VPrintf("attaching supplementary group ids %s", supp)
|
||||
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
|
||||
}
|
||||
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
s.cmd.Dir = "/"
|
||||
|
||||
// pass sync fd if set
|
||||
if s.payload.Bwrap.Sync() != nil {
|
||||
fd := proc.ExtraFile(s.cmd, s.payload.Bwrap.Sync())
|
||||
s.payload.Sync = &fd
|
||||
if syncFd != nil {
|
||||
fd := proc.ExtraFile(s.cmd, syncFd)
|
||||
s.sync = &fd
|
||||
}
|
||||
|
||||
fmsg.VPrintln("starting shim via fsu:", s.cmd)
|
||||
@ -101,7 +98,7 @@ func (s *Shim) Start() (*time.Time, error) {
|
||||
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
|
||||
s.killFallback = make(chan error, 1)
|
||||
killShim := func() {
|
||||
@ -111,8 +108,9 @@ func (s *Shim) Serve(ctx context.Context) error {
|
||||
}
|
||||
defer func() { killShim() }()
|
||||
|
||||
payload.Sync = s.sync
|
||||
encodeErr := make(chan error)
|
||||
go func() { encodeErr <- s.encoder.Encode(s.payload) }()
|
||||
go func() { encodeErr <- s.encoder.Encode(payload) }()
|
||||
|
||||
select {
|
||||
// encode return indicates setup completion
|
||||
|
@ -1,6 +1,8 @@
|
||||
package shim
|
||||
|
||||
import "git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
import (
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
)
|
||||
|
||||
const Env = "FORTIFY_SHIM"
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
@ -208,12 +210,11 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
|
||||
s := new(State)
|
||||
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 err = gob.NewDecoder(f).Decode(s); err != nil {
|
||||
if err = b.decodeState(f, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.ID != *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
|
||||
}
|
||||
|
||||
// state file consists of an eight byte header, followed by concatenated gobs
|
||||
// of [fst.Config] and [State], if [State.Config] is not nil or offset < 0,
|
||||
// the first gob is skipped
|
||||
func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
|
||||
offset := make([]byte, 8)
|
||||
if l, err := r.Read(offset); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("state file too short: %d bytes", l)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// decode volatile state first
|
||||
var skipConfig bool
|
||||
{
|
||||
o := int64(binary.LittleEndian.Uint64(offset))
|
||||
skipConfig = o < 0
|
||||
|
||||
if !skipConfig {
|
||||
if l, err := r.Seek(o, io.SeekCurrent); err != nil {
|
||||
return err
|
||||
} else if l != 8+o {
|
||||
return fmt.Errorf("invalid seek offset %d", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := gob.NewDecoder(r).Decode(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode sealed config
|
||||
if state.Config == nil {
|
||||
// config must be provided either as part of volatile state,
|
||||
// or in the config segment
|
||||
if skipConfig {
|
||||
return ErrNoConfig
|
||||
}
|
||||
|
||||
state.Config = new(fst.Config)
|
||||
if _, err := r.Seek(8, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
return gob.NewDecoder(r).Decode(state.Config)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Save writes process state to filesystem
|
||||
func (b *multiBackend) Save(state *State) error {
|
||||
func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
if state.Config == nil {
|
||||
return errors.New("state does not contain config")
|
||||
if configWriter == nil && state.Config == nil {
|
||||
return ErrNoConfig
|
||||
}
|
||||
|
||||
statePath := b.filename(&state.ID)
|
||||
|
||||
// create and open state data file
|
||||
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
||||
return err
|
||||
} else {
|
||||
@ -250,11 +298,43 @@ func (b *multiBackend) Save(state *State) error {
|
||||
panic("state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
// encode into state file
|
||||
return gob.NewEncoder(f).Encode(state)
|
||||
return b.encodeState(f, state, configWriter)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter io.WriterTo) error {
|
||||
offset := make([]byte, 8)
|
||||
|
||||
// skip header bytes
|
||||
if _, err := w.Seek(8, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if configWriter != nil {
|
||||
// write config gob and encode header
|
||||
if l, err := configWriter.WriteTo(w); err != nil {
|
||||
return err
|
||||
} else {
|
||||
binary.LittleEndian.PutUint64(offset, uint64(l))
|
||||
}
|
||||
} else {
|
||||
// offset == -1 indicates absence of config gob
|
||||
binary.LittleEndian.PutUint64(offset, 0xffffffffffffffff)
|
||||
}
|
||||
|
||||
// encode volatile state
|
||||
if err := gob.NewEncoder(w).Encode(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write header
|
||||
if _, err := w.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(offset)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *multiBackend) Destroy(id fst.ID) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
@ -1,11 +1,15 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
)
|
||||
|
||||
var ErrNoConfig = errors.New("state does not contain config")
|
||||
|
||||
type Entries map[fst.ID]*State
|
||||
|
||||
type Store interface {
|
||||
@ -24,13 +28,13 @@ type Store interface {
|
||||
|
||||
// Cursor provides access to the store
|
||||
type Cursor interface {
|
||||
Save(state *State) error
|
||||
Save(state *State, configWriter io.WriterTo) error
|
||||
Destroy(id fst.ID) error
|
||||
Load() (Entries, 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 {
|
||||
// fortify instance id
|
||||
ID fst.ID `json:"instance"`
|
||||
@ -40,5 +44,5 @@ type State struct {
|
||||
Config *fst.Config `json:"config"`
|
||||
|
||||
// process start time
|
||||
Time time.Time
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"reflect"
|
||||
"slices"
|
||||
@ -28,9 +31,12 @@ func testStore(t *testing.T, s state.Store) {
|
||||
tl
|
||||
)
|
||||
|
||||
var tc [tl]state.State
|
||||
var tc [tl]struct {
|
||||
state state.State
|
||||
ct bytes.Buffer
|
||||
}
|
||||
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)) {
|
||||
@ -41,7 +47,7 @@ func testStore(t *testing.T, s state.Store) {
|
||||
|
||||
insert := func(i, aid int) {
|
||||
do(aid, func(c state.Cursor) {
|
||||
if err := c.Save(&tc[i]); err != nil {
|
||||
if err := c.Save(&tc[i].state, &tc[i].ct); err != nil {
|
||||
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) {
|
||||
if entries, err := c.Load(); err != nil {
|
||||
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",
|
||||
&tc[i].ID)
|
||||
&tc[i].state.ID)
|
||||
} else {
|
||||
got.Time = tc[i].Time
|
||||
if !reflect.DeepEqual(got, &tc[i]) {
|
||||
got.Time = tc[i].state.Time
|
||||
tc[i].state.Config = fst.Template()
|
||||
if !reflect.DeepEqual(got, &tc[i].state) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@ -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 {
|
||||
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.Time = time.Now()
|
||||
}
|
||||
|
@ -93,13 +93,13 @@ func (d *DBus) apply(_ *I) error {
|
||||
ready := make(chan error, 1)
|
||||
|
||||
// background dbus proxy start
|
||||
if err := d.proxy.Start(ready, d.out, true); err != nil {
|
||||
if err := d.proxy.Start(ready, d.out, true, true); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
"cannot start message bus proxy:")
|
||||
}
|
||||
fmsg.VPrintln("starting message bus proxy:", d.proxy)
|
||||
if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
|
||||
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
|
||||
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.BwrapStatic())
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
if b, err := helper.NewBwrap((&bwrap.Config{
|
||||
if b, err := helper.NewBwrap(
|
||||
(&bwrap.Config{
|
||||
Hostname: "fortify-ldd",
|
||||
Chdir: "/",
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||
NewSession: true,
|
||||
DieWithParent: true,
|
||||
}).Bind("/", "/").DevTmpfs("/dev"),
|
||||
nil, "ldd", func(_, _ int) []string { return []string{p} }); err != nil {
|
||||
}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
|
||||
nil, func(_, _ int) []string { return []string{p} },
|
||||
nil, nil,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
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/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
@ -308,6 +309,10 @@ func runApp(config *fst.Config) {
|
||||
rs := new(app.RunState)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
if fmsg.Verbose() {
|
||||
bwrap.CPrintln = fmsg.Println
|
||||
}
|
||||
|
||||
// handle signals for graceful shutdown
|
||||
sig := make(chan os.Signal, 2)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
88
nixos.nix
88
nixos.nix
@ -7,6 +7,7 @@
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
mkMerge
|
||||
mkIf
|
||||
mkDefault
|
||||
mapAttrs
|
||||
@ -19,6 +20,10 @@ let
|
||||
;
|
||||
|
||||
cfg = config.environment.fortify;
|
||||
|
||||
getsubuid = fid: aid: 1000000 + fid * 10000 + aid;
|
||||
getsubname = fid: aid: "u${toString fid}_a${toString aid}";
|
||||
getsubhome = fid: aid: "${cfg.stateDir}/u${toString fid}/a${toString aid}";
|
||||
in
|
||||
|
||||
{
|
||||
@ -33,8 +38,7 @@ in
|
||||
group = "root";
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
fsurc = {
|
||||
environment.etc.fsurc = {
|
||||
mode = "0400";
|
||||
text = foldlAttrs (
|
||||
acc: username: fid:
|
||||
@ -42,16 +46,6 @@ in
|
||||
) "" 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" ];
|
||||
|
||||
services.userdbd.enable = mkDefault true;
|
||||
@ -114,8 +108,8 @@ in
|
||||
confinement = {
|
||||
app_id = aid;
|
||||
inherit (app) groups;
|
||||
username = "u${toString fid}_a${toString aid}";
|
||||
home = "${cfg.stateDir}/u${toString fid}/a${toString aid}";
|
||||
username = getsubname fid aid;
|
||||
home = getsubhome fid aid;
|
||||
sandbox = {
|
||||
inherit (app)
|
||||
userns
|
||||
@ -123,6 +117,9 @@ in
|
||||
dev
|
||||
env
|
||||
;
|
||||
syscall = {
|
||||
inherit (app) devel multiarch bluetooth;
|
||||
};
|
||||
map_real_uid = app.mapRealUid;
|
||||
no_new_session = app.tty;
|
||||
filesystem =
|
||||
@ -173,7 +170,9 @@ in
|
||||
};
|
||||
in
|
||||
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;
|
||||
in
|
||||
@ -205,16 +204,63 @@ in
|
||||
|
||||
users = foldlAttrs (
|
||||
acc: _: fid:
|
||||
mergeAttrsList (
|
||||
mkMerge [
|
||||
(mergeAttrsList (
|
||||
# aid 0 is reserved
|
||||
imap1 (aid: app: {
|
||||
"u${toString fid}_a${toString aid}" = app.extraConfig // {
|
||||
home.packages = app.packages;
|
||||
};
|
||||
${getsubname fid aid} = mkMerge [
|
||||
(cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
|
||||
app.extraConfig
|
||||
{ home.packages = app.packages; }
|
||||
];
|
||||
}) cfg.apps
|
||||
)
|
||||
// acc
|
||||
))
|
||||
{ ${getsubname fid 0} = cfg.home-manager (getsubname fid 0) (getsubuid fid 0); }
|
||||
acc
|
||||
]
|
||||
) privPackages cfg.users;
|
||||
};
|
||||
|
||||
users =
|
||||
let
|
||||
getuser = fid: aid: {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
description = "Fortify subordinate user ${toString aid} (u${toString fid})";
|
||||
group = getsubname fid aid;
|
||||
home = getsubhome fid aid;
|
||||
uid = getsubuid fid aid;
|
||||
};
|
||||
getgroup = fid: aid: { gid = getsubuid fid aid; };
|
||||
in
|
||||
{
|
||||
users = foldlAttrs (
|
||||
acc: _: fid:
|
||||
mkMerge [
|
||||
(mergeAttrsList (
|
||||
# aid 0 is reserved
|
||||
imap1 (aid: _: {
|
||||
${getsubname fid aid} = getuser fid aid;
|
||||
}) cfg.apps
|
||||
))
|
||||
{ ${getsubname fid 0} = getuser fid 0; }
|
||||
acc
|
||||
]
|
||||
) { } cfg.users;
|
||||
|
||||
groups = foldlAttrs (
|
||||
acc: _: fid:
|
||||
mkMerge [
|
||||
(mergeAttrsList (
|
||||
# aid 0 is reserved
|
||||
imap1 (aid: _: {
|
||||
${getsubname fid aid} = getgroup fid aid;
|
||||
}) cfg.apps
|
||||
))
|
||||
{ ${getsubname fid 0} = getgroup fid 0; }
|
||||
acc
|
||||
]
|
||||
) { } cfg.users;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
124
options.md
124
options.md
@ -36,7 +36,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation fortify-0.2.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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
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 {
|
||||
type =
|
||||
let
|
||||
@ -50,6 +61,8 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
verbose = mkEnableOption "launchers with verbose output";
|
||||
|
||||
id = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
@ -128,16 +141,20 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
nix = mkEnableOption "nix daemon access within the sandbox";
|
||||
userns = mkEnableOption "userns within the sandbox";
|
||||
mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
|
||||
dev = mkEnableOption "access to all devices within the sandbox";
|
||||
tty = mkEnableOption "allow access to the controlling terminal";
|
||||
nix = mkEnableOption "nix daemon";
|
||||
userns = mkEnableOption "user namespace";
|
||||
mapRealUid = mkEnableOption "mapping to priv-user uid";
|
||||
dev = mkEnableOption "access to all devices";
|
||||
tty = mkEnableOption "access to the controlling terminal";
|
||||
|
||||
net = mkEnableOption "network access within the sandbox" // {
|
||||
net = mkEnableOption "network access" // {
|
||||
default = true;
|
||||
};
|
||||
|
||||
devel = mkEnableOption "development kernel APIs";
|
||||
multiarch = mkEnableOption "multiarch kernel support";
|
||||
bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
|
||||
|
||||
gpu = mkOption {
|
||||
type = nullOr bool;
|
||||
default = null;
|
||||
|
@ -6,6 +6,7 @@
|
||||
bubblewrap,
|
||||
pkg-config,
|
||||
libffi,
|
||||
libseccomp,
|
||||
acl,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
@ -15,7 +16,7 @@
|
||||
|
||||
buildGoModule rec {
|
||||
pname = "fortify";
|
||||
version = "0.2.10";
|
||||
version = "0.2.11";
|
||||
|
||||
src = builtins.path {
|
||||
name = "fortify-src";
|
||||
@ -45,6 +46,7 @@ buildGoModule rec {
|
||||
buildInputs =
|
||||
[
|
||||
libffi
|
||||
libseccomp
|
||||
acl
|
||||
wayland
|
||||
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()
|
||||
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
|
||||
|
||||
if config.Confinement.Sandbox == nil {
|
||||
fmt.Print("Warning: this configuration uses permissive defaults!\n\n")
|
||||
}
|
||||
|
||||
if instance != nil {
|
||||
fmt.Fprintf(w, "State\n")
|
||||
fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
|
||||
@ -106,9 +110,6 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
|
||||
|
||||
// Env map[string]string `json:"env"`
|
||||
// Link [][2]string `json:"symlink"`
|
||||
} else {
|
||||
// this gets printed before everything else
|
||||
fmt.Println("WARNING: current configuration uses permissive defaults!")
|
||||
}
|
||||
fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
98
test.nix
98
test.nix
@ -44,7 +44,6 @@ nixosTest {
|
||||
# For glinfo and wayland-info:
|
||||
mesa-demos
|
||||
wayland-utils
|
||||
alacritty
|
||||
|
||||
# For D-Bus tests:
|
||||
libnotify
|
||||
@ -83,7 +82,7 @@ nixosTest {
|
||||
sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
|
||||
|
||||
sway --validate
|
||||
sway && touch /tmp/sway-exit-ok
|
||||
systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
|
||||
fi
|
||||
'';
|
||||
|
||||
@ -111,6 +110,43 @@ nixosTest {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/fortify";
|
||||
users.alice = 0;
|
||||
|
||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||
|
||||
apps = [
|
||||
{
|
||||
name = "ne-foot";
|
||||
verbose = true;
|
||||
share = pkgs.foot;
|
||||
packages = [ pkgs.foot ];
|
||||
command = "foot";
|
||||
capability = {
|
||||
dbus = false;
|
||||
pulse = false;
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "pa-foot";
|
||||
verbose = true;
|
||||
share = pkgs.foot;
|
||||
packages = [ pkgs.foot ];
|
||||
command = "foot";
|
||||
capability.dbus = false;
|
||||
}
|
||||
{
|
||||
name = "x11-alacritty";
|
||||
verbose = true;
|
||||
share = pkgs.alacritty;
|
||||
packages = [ pkgs.alacritty ];
|
||||
command = "alacritty";
|
||||
capability = {
|
||||
wayland = false;
|
||||
x11 = true;
|
||||
dbus = false;
|
||||
pulse = false;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
imports = [
|
||||
@ -176,16 +212,18 @@ nixosTest {
|
||||
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"))
|
||||
if len(instances) != 1:
|
||||
raise Exception(f"unexpected state length {len(instances)}")
|
||||
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']}")
|
||||
|
||||
if instance['config']['confinement']['enablements'] != enablements:
|
||||
if config['confinement']['enablements'] != enablements:
|
||||
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
|
||||
|
||||
|
||||
@ -212,60 +250,60 @@ nixosTest {
|
||||
# Create fortify uid 0 state directory:
|
||||
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"))
|
||||
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')
|
||||
machine.wait_for_file("/tmp/dbus-done")
|
||||
collect_state_ui("dbus_notify_exited")
|
||||
machine.succeed("pkill -9 mako")
|
||||
|
||||
# Start a terminal (foot) within fortify:
|
||||
fortify("run --wayland foot")
|
||||
wait_for_window("u0_a0@machine")
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec ne-foot")
|
||||
wait_for_window("u0_a1@machine")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
|
||||
collect_state_ui("foot_wayland_permissive")
|
||||
check_state(["foot"], 1)
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
|
||||
collect_state_ui("foot_wayland")
|
||||
check_state("ne-foot", 1)
|
||||
# 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.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 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:
|
||||
swaymsg("exec foot $SHELL -c '(fortify run --wayland foot) & sleep 1 && fortify show --short $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
||||
wait_for_window("u0_a0@machine")
|
||||
# Start app (foot) with Wayland enablement from a terminal:
|
||||
swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
||||
wait_for_window("u0_a1@machine")
|
||||
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")
|
||||
collect_state_ui("foot_wayland_permissive_term")
|
||||
check_state(["foot"], 1)
|
||||
collect_state_ui("foot_wayland_term")
|
||||
check_state("ne-foot", 1)
|
||||
machine.send_chars("exit\n")
|
||||
wait_for_window("foot")
|
||||
machine.send_key("ctrl-c")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
|
||||
# Test PulseAudio (fortify does not support PipeWire yet):
|
||||
fortify("run --wayland --pulse foot")
|
||||
wait_for_window("u0_a0@machine")
|
||||
swaymsg("exec pa-foot")
|
||||
wait_for_window("u0_a2@machine")
|
||||
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")
|
||||
check_state(["foot"], 9)
|
||||
check_state("pa-foot", 9)
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
|
||||
# Test XWayland (foot does not support X):
|
||||
fortify("run -X alacritty")
|
||||
wait_for_window("u0_a0@machine")
|
||||
swaymsg("exec x11-alacritty")
|
||||
wait_for_window("u0_a3@machine")
|
||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11")
|
||||
collect_state_ui("alacritty_x11_permissive")
|
||||
check_state(["alacritty"], 2)
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
|
||||
collect_state_ui("alacritty_x11")
|
||||
check_state("x11-alacritty", 2)
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep alacritty")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user