helper/bwrap: integrate seccomp into helper interface
All checks were successful
Build / Create distribution (push) Successful in 1m36s
Test / Run NixOS test (push) Successful in 3m40s

This makes API usage much cleaner, and encapsulates all bwrap arguments in argsWt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-01-22 01:51:10 +09:00
parent 82029948e6
commit 9a239fa1a5
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
21 changed files with 224 additions and 187 deletions

View File

@ -66,7 +66,7 @@ func (p *Proxy) String() string {
return "(unsealed dbus proxy)"
}
func (p *Proxy) Bwrap() []string {
func (p *Proxy) BwrapStatic() []string {
return p.bwrap.Args()
}

View File

@ -110,7 +110,7 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
bc.Bind(k, k)
}
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil)
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
cmd = h.Unwrap()
p.bwrap = bc
}

View File

@ -31,8 +31,6 @@ type ConfinementConfig struct {
Outer string `json:"home"`
// bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"`
// seccomp syscall filter configuration
Syscall *SyscallConfig `json:"syscall"`
// extra acl entries to append
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -47,14 +45,6 @@ type ConfinementConfig struct {
Enablements system.Enablements `json:"enablements"`
}
type SyscallConfig struct {
DenyDevel bool `json:"deny_devel"`
Multiarch bool `json:"multiarch"`
Linux32 bool `json:"linux32"`
Can bool `json:"can"`
Bluetooth bool `json:"bluetooth"`
}
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`

View File

@ -22,6 +22,8 @@ type SandboxConfig struct {
Net bool `json:"net,omitempty"`
// share all devices
Dev bool `json:"dev,omitempty"`
// seccomp syscall filter policy
Syscall *bwrap.SyscallPolicy `json:"syscall"`
// do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace
@ -50,6 +52,10 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
return nil, errors.New("nil sandbox config")
}
if s.Syscall == nil {
fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
}
var uid int
if !s.MapRealUID {
uid = 65534
@ -69,6 +75,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
so this capacity should eliminate copies for most setups */
Filesystem: make([]bwrap.FSBuilder, 0, 256),
Syscall: s.Syscall,
NewSession: !s.NoNewSession,
DieWithParent: true,
AsInit: true,

View File

@ -9,25 +9,17 @@ import (
"sync"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/proc"
)
// BubblewrapName is the file name or path to bubblewrap.
var BubblewrapName = "bwrap"
type BwrapExtraFile struct {
Name string
File *os.File
}
type bubblewrap struct {
// bwrap child file name
name string
// bwrap pipes
control *pipes
// extra files with fd passed as argument
extra []BwrapExtraFile
// returns an array of arguments passed directly
// to the child process spawned by bwrap
argF func(argsFD, statFD int) []string
@ -54,14 +46,6 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
return errors.New("exec: already started")
}
// pass extra fd to bwrap
for _, e := range b.extra {
if e.File == nil {
continue
}
b.Cmd.Args = append(b.Cmd.Args, e.Name, strconv.Itoa(int(proc.ExtraFile(b.Cmd, e.File))))
}
// prepare bwrap pipe and args
if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
return err
@ -130,9 +114,10 @@ func (b *bubblewrap) Unwrap() *exec.Cmd {
func MustNewBwrap(
conf *bwrap.Config, name string,
wt io.WriterTo, argF func(argsFD, statFD int) []string,
extra []BwrapExtraFile,
extraFiles []*os.File,
syncFd *os.File,
) Helper {
b, err := NewBwrap(conf, name, wt, argF, extra)
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
if err != nil {
panic(err.Error())
} else {
@ -146,23 +131,27 @@ func MustNewBwrap(
func NewBwrap(
conf *bwrap.Config, name string,
wt io.WriterTo, argF func(argsFD, statFD int) []string,
extra []BwrapExtraFile,
extraFiles []*os.File,
syncFd *os.File,
) (Helper, error) {
b := new(bubblewrap)
if args, err := NewCheckedArgs(conf.Args()); err != nil {
return nil, err
} else {
b.control = &pipes{args: args}
}
b.extra = extra
b.argF = argF
b.name = name
if wt != nil {
b.controlPt = &pipes{args: wt}
}
b.Cmd = execCommand(BubblewrapName)
b.control = new(pipes)
args := conf.Args()
if fdArgs, err := conf.FDArgs(syncFd, &extraFiles); err != nil {
return nil, err
} else if b.control.args, err = NewCheckedArgs(append(args, fdArgs...)); err != nil {
return nil, err
} else {
b.Cmd.ExtraFiles = extraFiles
}
return b, nil
}

View File

@ -1,6 +1,13 @@
package bwrap
import "encoding/gob"
import (
"encoding/gob"
"os"
"slices"
"strconv"
"git.gensokyo.uk/security/fortify/internal/proc"
)
type Builder interface {
Len() int
@ -12,6 +19,11 @@ type FSBuilder interface {
Builder
}
type FDBuilder interface {
Len() int
Append(args *[]string, extraFiles *[]*os.File) error
}
func init() {
gob.Register(new(pairF))
gob.Register(new(stringF))
@ -45,6 +57,33 @@ func (s stringF) Append(args *[]string) {
*args = append(*args, s[0], s[1])
}
type fileF struct {
name string
file *os.File
}
func (f *fileF) Len() int {
if f.file == nil {
return 0
}
return 2
}
func (f *fileF) Append(args *[]string, extraFiles *[]*os.File) error {
if f.file == nil {
return nil
}
extraFile(args, extraFiles, f.name, f.file)
return nil
}
func extraFile(args *[]string, extraFiles *[]*os.File, name string, f *os.File) {
if f == nil {
return
}
*args = append(*args, name, strconv.Itoa(int(proc.ExtraFileSlice(extraFiles, f))))
}
// Args returns a slice of bwrap args corresponding to c.
func (c *Config) Args() (args []string) {
builders := []Builder{
@ -75,3 +114,25 @@ func (c *Config) Args() (args []string) {
return
}
func (c *Config) FDArgs(syncFd *os.File, extraFiles *[]*os.File) (args []string, err error) {
builders := []FDBuilder{
&seccompBuilder{c},
&fileF{positionalArgs[SyncFd], syncFd},
}
argc := 0
for _, b := range builders {
argc += b.Len()
}
args = make([]string, 0, argc)
*extraFiles = slices.Grow(*extraFiles, len(builders))
for _, b := range builders {
if err = b.Append(&args, extraFiles); err != nil {
break
}
}
return
}

View File

@ -47,6 +47,10 @@ type Config struct {
// (--chmod OCTAL PATH)
Chmod ChmodConfig `json:"chmod,omitempty"`
// load and use seccomp rules from FD (not repeatable)
// (--seccomp FD)
Syscall *SyscallPolicy
// create a new terminal session
// (--new-session)
NewSession bool `json:"new_session"`
@ -70,7 +74,6 @@ type Config struct {
--file FD DEST Copy from FD to destination DEST
--bind-data FD DEST Copy from FD to file which is bind-mounted on DEST
--ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST
--seccomp FD Load and use seccomp rules from FD (not repeatable)
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
--block-fd FD Block on FD until some data to read is available
--userns-block-fd FD Block on FD until the user namespace is ready

View File

@ -2,7 +2,7 @@
#define _GNU_SOURCE // CLONE_NEWUSER
#endif
#include "export.h"
#include "seccomp-export.h"
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

View 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
}

View File

@ -1,9 +1,9 @@
package shim
package bwrap
/*
#cgo linux pkg-config: --static libseccomp
#include "export.h"
#include "seccomp-export.h"
*/
import "C"
import (
@ -11,10 +11,10 @@ import (
"fmt"
"os"
"runtime"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
var CPrintln func(v ...any)
var resErr = [...]error{
0: nil,
1: errors.New("seccomp_init failed"),
@ -77,5 +77,7 @@ func exportFilter(fd uintptr, opts syscallOpts) error {
//export F_println
func F_println(v *C.char) {
fmsg.VPrintln(C.GoString(v))
if CPrintln != nil {
CPrintln(C.GoString(v))
}
}

View File

@ -43,6 +43,9 @@ const (
Overlay
TmpOverlay
ROOverlay
SyncFd
Seccomp
)
var positionalArgs = [...]string{
@ -70,6 +73,9 @@ var positionalArgs = [...]string{
Overlay: "--overlay",
TmpOverlay: "--tmp-overlay",
ROOverlay: "--ro-overlay",
SyncFd: "--sync-fd",
Seccomp: "--seccomp",
}
type PermConfig[T FSBuilder] struct {

View File

@ -34,7 +34,7 @@ func TestBwrap(t *testing.T) {
h := helper.MustNewBwrap(
sc, "fortify",
argsWt, argF,
nil,
nil, nil,
)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
@ -47,7 +47,7 @@ func TestBwrap(t *testing.T) {
if got := helper.MustNewBwrap(
sc, "fortify",
argsWt, argF,
nil,
nil, nil,
); got == nil {
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
sc, argsWt, "fortify")
@ -67,7 +67,7 @@ func TestBwrap(t *testing.T) {
helper.MustNewBwrap(
&bwrap.Config{Hostname: "\x00"}, "fortify",
nil, argF,
nil,
nil, nil,
)
})
@ -84,7 +84,7 @@ func TestBwrap(t *testing.T) {
helper.MustNewBwrap(
sc, "fortify",
nil, argF,
nil,
nil, nil,
).StartNotify(make(chan error))))
})
@ -94,7 +94,7 @@ func TestBwrap(t *testing.T) {
h := helper.MustNewBwrap(
sc, "crash-test-dummy",
nil, argFChecked,
nil,
nil, nil,
)
cmd := h.Unwrap()
@ -127,6 +127,6 @@ func TestBwrap(t *testing.T) {
})
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil) })
testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
})
}

View File

@ -39,6 +39,7 @@ var testCasesPd = []sealTestCase{
Net: true,
UserNS: true,
Clearenv: true,
Syscall: new(bwrap.SyscallPolicy),
Chdir: "/home/chronos",
SetEnv: map[string]string{
"HOME": "/home/chronos",
@ -258,6 +259,7 @@ var testCasesPd = []sealTestCase{
UserNS: true,
Chdir: "/home/chronos",
Clearenv: true,
Syscall: new(bwrap.SyscallPolicy),
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",

View File

@ -14,6 +14,7 @@ import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state"
@ -52,8 +53,6 @@ type appSeal struct {
et system.Enablements
// initial config gob encoding buffer
ct io.WriterTo
// pass-through seccomp config from config
scmp *fst.SyscallConfig
// wayland socket direct access
directWayland bool
// extra UpdatePerm ops
@ -196,6 +195,7 @@ func (a *app) Seal(config *fst.Config) error {
conf := &fst.SandboxConfig{
UserNS: true,
Net: true,
Syscall: new(bwrap.SyscallPolicy),
NoNewSession: true,
AutoEtc: true,
}
@ -233,12 +233,6 @@ func (a *app) Seal(config *fst.Config) error {
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
config.Confinement.Sandbox = conf
// ensure syscall filter
if config.Confinement.Syscall == nil {
config.Confinement.Syscall = new(fst.SyscallConfig)
config.Confinement.Syscall.Multiarch = true
}
}
seal.directWayland = config.Confinement.Sandbox.DirectWayland
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
@ -259,9 +253,8 @@ func (a *app) Seal(config *fst.Config) error {
// initialise system interface with full uid
seal.sys.I = system.New(seal.sys.user.uid)
// pass through enablements and seccomp
// pass through enablements
seal.et = config.Confinement.Enablements
seal.scmp = config.Confinement.Syscall
// this method calls all share methods in sequence
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {

View File

@ -76,11 +76,10 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
// send payload
if err = a.shim.Serve(shimSetupCtx, &shim.Payload{
Argv: a.seal.command,
Exec: shimExec,
Bwrap: a.seal.sys.bwrap,
Home: a.seal.sys.user.data,
Syscall: a.seal.scmp,
Argv: a.seal.command,
Exec: shimExec,
Bwrap: a.seal.sys.bwrap,
Home: a.seal.sys.user.data,
Verbose: fmsg.Verbose(),
}); err != nil {

View File

@ -2,8 +2,6 @@ package shim
import (
"errors"
"flag"
"io"
"os"
"path"
"strconv"
@ -20,7 +18,7 @@ import (
// everything beyond this point runs as unconstrained target user
// proceed with caution!
func Main(args []string) {
func Main() {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.SetPrefix("shim")
@ -31,46 +29,6 @@ func Main(args []string) {
panic("unreachable")
}
set := flag.NewFlagSet("shim", flag.ExitOnError)
// debug: export seccomp filter
debugExportSeccomp := set.String("export-seccomp", "", "export the seccomp filter to file")
debugExportSeccompFlags := [...]struct {
o syscallOpts
v *bool
}{
{flagDenyNS, set.Bool("deny-ns", false, "deny namespace-related syscalls")},
{flagDenyTTY, set.Bool("deny-tty", false, "deny faking input ioctls")},
{flagDenyDevel, set.Bool("deny-devel", false, "deny development syscalls")},
{flagMultiarch, set.Bool("multiarch", false, "allow multiarch")},
{flagLinux32, set.Bool("linux32", false, "allow PER_LINUX32")},
{flagCan, set.Bool("can", false, "allow AF_CAN")},
{flagBluetooth, set.Bool("bluetooth", false, "AF_BLUETOOTH")},
}
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
// debug: export seccomp filter
if *debugExportSeccomp != "" {
var opts syscallOpts
for _, opt := range debugExportSeccompFlags {
if *opt.v {
opts |= opt.o
}
}
if f, err := os.Create(*debugExportSeccomp); err != nil {
fmsg.Fatalf("cannot create %q: %v", *debugExportSeccomp, err)
} else {
mustExportFilter(f, opts)
if err = f.Close(); err != nil {
fmsg.Fatalf("cannot close %q: %v", *debugExportSeccomp, err)
}
}
fmsg.Exit(0)
}
// receive setup payload
var (
payload Payload
@ -169,23 +127,19 @@ func Main(args []string) {
conf.Symlink("fortify", innerInit)
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if fmsg.Verbose() {
bwrap.CPrintln = fmsg.Println
}
if b, err := helper.NewBwrap(
conf, innerInit,
nil, func(int, int) []string { return make([]string, 0) },
[]helper.BwrapExtraFile{
// keep this fd open while sandbox is running
// (--sync-fd FD)
{"--sync-fd", syncFd},
// load and use seccomp rules from FD (not repeatable)
// (--seccomp FD)
{"--seccomp", mustResolveSeccomp(payload.Bwrap, payload.Syscall)},
},
extraFiles,
syncFd,
); err != nil {
fmsg.Fatalf("malformed sandbox config: %v", err)
} else {
cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = extraFiles
// run and pass through exit code
if err = b.Start(); err != nil {
@ -200,65 +154,3 @@ func Main(args []string) {
}
}
}
func mustResolveSeccomp(bwrap *bwrap.Config, syscall *fst.SyscallConfig) (seccompFd *os.File) {
if syscall == nil {
fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
return
}
// resolve seccomp filter opts
var (
opts syscallOpts
optd []string
optCond = [...]struct {
v bool
o syscallOpts
d string
}{
{!bwrap.UserNS, flagDenyNS, "denyns"},
{bwrap.NewSession, flagDenyTTY, "denytty"},
{syscall.DenyDevel, flagDenyDevel, "denydevel"},
{syscall.Multiarch, flagMultiarch, "multiarch"},
{syscall.Linux32, flagLinux32, "linux32"},
{syscall.Can, flagCan, "can"},
{syscall.Bluetooth, flagBluetooth, "bluetooth"},
}
)
if fmsg.Verbose() {
optd = make([]string, 1, len(optCond)+1)
optd[0] = "fortify"
}
for _, opt := range optCond {
if opt.v {
opts |= opt.o
if fmsg.Verbose() {
optd = append(optd, opt.d)
}
}
}
if fmsg.Verbose() {
fmsg.VPrintf("seccomp flags: %s", optd)
}
// export seccomp filter to tmpfile
if f, err := tmpfile(); err != nil {
fmsg.Fatalf("cannot create tmpfile: %v", err)
panic("unreachable")
} else {
mustExportFilter(f, opts)
seccompFd = f
return
}
}
func mustExportFilter(f *os.File, opts syscallOpts) {
if err := exportFilter(f.Fd(), opts); err != nil {
fmsg.Fatalf("cannot export seccomp filter: %v", err)
panic("unreachable")
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
fmsg.Fatalf("cannot lseek seccomp file: %v", err)
panic("unreachable")
}
}

View File

@ -1,7 +1,6 @@
package shim
import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
@ -18,8 +17,6 @@ type Payload struct {
Home string
// sync fd
Sync *uintptr
// seccomp opts pass through
Syscall *fst.SyscallConfig
// verbosity pass through
Verbose bool

View File

@ -99,7 +99,7 @@ func (d *DBus) apply(_ *I) error {
}
fmsg.VPrintln("starting message bus proxy:", d.proxy)
if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.BwrapStatic())
}
// background wait for proxy instance and notify completion

View File

@ -23,7 +23,8 @@ func Exec(p string) ([]*Entry, error) {
NewSession: true,
DieWithParent: true,
}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
nil, func(_, _ int) []string { return []string{p} }, nil,
nil, func(_, _ int) []string { return []string{p} },
nil, nil,
); err != nil {
return nil, err
} else {

View File

@ -291,7 +291,7 @@ func main() {
// internal commands
case "shim":
shim.Main(args)
shim.Main()
fmsg.Exit(0)
case "init":
init0.Main()