Compare commits
21 Commits
1ec901f79e
...
8a00a83c71
Author | SHA1 | Date | |
---|---|---|---|
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,22 +75,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// shim accepted setup payload, create process state
|
// shim accepted setup payload, create process state
|
||||||
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
|
||||||
|
18
ldd/exec.go
18
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(
|
||||||
Hostname: "fortify-ldd",
|
(&bwrap.Config{
|
||||||
Chdir: "/",
|
Hostname: "fortify-ldd",
|
||||||
NewSession: true,
|
Chdir: "/",
|
||||||
DieWithParent: true,
|
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||||
}).Bind("/", "/").DevTmpfs("/dev"),
|
NewSession: true,
|
||||||
nil, "ldd", func(_, _ int) []string { return []string{p} }); err != nil {
|
DieWithParent: true,
|
||||||
|
}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
|
||||||
|
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)
|
||||||
|
89
nixos.nix
89
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,23 +38,12 @@ 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:
|
"${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
|
||||||
"${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
|
) "" 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" ];
|
||||||
@ -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
|
||||||
@ -208,13 +207,57 @@ in
|
|||||||
mergeAttrsList (
|
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
|
// 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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
@ -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…
Reference in New Issue
Block a user