helper/bwrap: separate sequential/static args
All checks were successful
Tests / Go tests (push) Successful in 41s
Nix / NixOS tests (push) Successful in 3m59s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-01-15 10:57:10 +09:00
parent 562f5ed797
commit 3e11ce6868
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
11 changed files with 663 additions and 627 deletions

View File

@ -20,7 +20,7 @@ type bubblewrap struct {
name string name string
// bwrap pipes // bwrap pipes
p *pipes control *pipes
// sync pipe // sync pipe
sync *os.File sync *os.File
// returns an array of arguments passed directly // returns an array of arguments passed directly
@ -29,7 +29,7 @@ type bubblewrap struct {
// pipes received by the child // pipes received by the child
// nil if no pipes are required // nil if no pipes are required
cp *pipes controlPt *pipes
lock sync.RWMutex lock sync.RWMutex
*exec.Cmd *exec.Cmd
@ -39,7 +39,7 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
b.lock.Lock() b.lock.Lock()
defer b.lock.Unlock() defer b.lock.Unlock()
if ready != nil && b.cp == nil { if ready != nil && b.controlPt == nil {
panic("attempted to start with status monitoring on a bwrap child initialised without pipes") panic("attempted to start with status monitoring on a bwrap child initialised without pipes")
} }
@ -50,16 +50,16 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
} }
// prepare bwrap pipe and args // prepare bwrap pipe and args
if argsFD, _, err := b.p.prepareCmd(b.Cmd); err != nil { if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil {
return err return err
} else { } else {
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name) b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name)
} }
// prepare child args and pipes if enabled // prepare child args and pipes if enabled
if b.cp != nil { if b.controlPt != nil {
b.cp.ready = ready b.controlPt.ready = ready
if argsFD, statFD, err := b.cp.prepareCmd(b.Cmd); err != nil { if argsFD, statFD, err := b.controlPt.prepareCmd(b.Cmd); err != nil {
return err return err
} else { } else {
b.Cmd.Args = append(b.Cmd.Args, b.argF(argsFD, statFD)...) b.Cmd.Args = append(b.Cmd.Args, b.argF(argsFD, statFD)...)
@ -70,7 +70,7 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
if ready != nil { if ready != nil {
b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1") b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1")
} else if b.cp != nil { } else if b.controlPt != nil {
b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0") b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0")
} else { } else {
b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1") b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
@ -85,13 +85,13 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
} }
// write bwrap args first // write bwrap args first
if err := b.p.readyWriteArgs(); err != nil { if err := b.control.readyWriteArgs(); err != nil {
return err return err
} }
// write child args if enabled // write child args if enabled
if b.cp != nil { if b.controlPt != nil {
if err := b.cp.readyWriteArgs(); err != nil { if err := b.controlPt.readyWriteArgs(); err != nil {
return err return err
} }
} }
@ -100,11 +100,11 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
} }
func (b *bubblewrap) Close() error { func (b *bubblewrap) Close() error {
if b.cp == nil { if b.controlPt == nil {
panic("attempted to close bwrap child initialised without pipes") panic("attempted to close bwrap child initialised without pipes")
} }
return b.cp.closeStatus() return b.controlPt.closeStatus()
} }
func (b *bubblewrap) Start() error { func (b *bubblewrap) Start() error {
@ -136,14 +136,14 @@ func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD,
if args, err := NewCheckedArgs(conf.Args()); err != nil { if args, err := NewCheckedArgs(conf.Args()); err != nil {
return nil, err return nil, err
} else { } else {
b.p = &pipes{args: args} b.control = &pipes{args: args}
} }
b.sync = conf.Sync() b.sync = conf.Sync()
b.argF = argF b.argF = argF
b.name = name b.name = name
if wt != nil { if wt != nil {
b.cp = &pipes{args: wt} b.controlPt = &pipes{args: wt}
} }
b.Cmd = execCommand(BubblewrapName) b.Cmd = execCommand(BubblewrapName)

View File

@ -1,23 +0,0 @@
package bwrap
const (
Tmpfs = iota
Dir
Symlink
OverlaySrc
Overlay
TmpOverlay
ROOverlay
)
var awkwardArgs = [...]string{
Tmpfs: "--tmpfs",
Dir: "--dir",
Symlink: "--symlink",
OverlaySrc: "--overlay-src",
Overlay: "--overlay",
TmpOverlay: "--tmp-overlay",
ROOverlay: "--ro-overlay",
}

View File

@ -1,81 +0,0 @@
package bwrap
const (
UnshareAll = iota
UnshareUser
UnshareIPC
UnsharePID
UnshareNet
UnshareUTS
UnshareCGroup
ShareNet
UserNS
Clearenv
NewSession
DieWithParent
AsInit
)
var boolArgs = [...][]string{
UnshareAll: {"--unshare-all", "--unshare-user"},
UnshareUser: {"--unshare-user"},
UnshareIPC: {"--unshare-ipc"},
UnsharePID: {"--unshare-pid"},
UnshareNet: {"--unshare-net"},
UnshareUTS: {"--unshare-uts"},
UnshareCGroup: {"--unshare-cgroup"},
ShareNet: {"--share-net"},
UserNS: {"--disable-userns", "--assert-userns-disabled"},
Clearenv: {"--clearenv"},
NewSession: {"--new-session"},
DieWithParent: {"--die-with-parent"},
AsInit: {"--as-pid-1"},
}
func (c *Config) boolArgs() Builder {
b := boolArg{
UserNS: !c.UserNS,
Clearenv: c.Clearenv,
NewSession: c.NewSession,
DieWithParent: c.DieWithParent,
AsInit: c.AsInit,
}
if c.Unshare == nil {
b[UnshareAll] = true
b[ShareNet] = c.Net
} else {
b[UnshareUser] = c.Unshare.User
b[UnshareIPC] = c.Unshare.IPC
b[UnsharePID] = c.Unshare.PID
b[UnshareNet] = c.Unshare.Net
b[UnshareUTS] = c.Unshare.UTS
b[UnshareCGroup] = c.Unshare.CGroup
}
return &b
}
type boolArg [len(boolArgs)]bool
func (b *boolArg) Len() (l int) {
for i, v := range b {
if v {
l += len(boolArgs[i])
}
}
return
}
func (b *boolArg) Append(args *[]string) {
for i, v := range b {
if v {
*args = append(*args, boolArgs[i]...)
}
}
}

View File

@ -1,47 +0,0 @@
package bwrap
import "strconv"
const (
UID = iota
GID
Perms
Size
)
var intArgs = [...]string{
UID: "--uid",
GID: "--gid",
Perms: "--perms",
Size: "--size",
}
func (c *Config) intArgs() Builder {
// Arg types:
// Perms
// are handled by the sequential builder
return &intArg{
UID: c.UID,
GID: c.GID,
}
}
type intArg [len(intArgs)]*int
func (n *intArg) Len() (l int) {
for _, v := range n {
if v != nil {
l += 2
}
}
return
}
func (n *intArg) Append(args *[]string) {
for i, v := range n {
if v != nil {
*args = append(*args, intArgs[i], strconv.Itoa(*v))
}
}
}

View File

@ -1,73 +0,0 @@
package bwrap
import (
"slices"
)
const (
SetEnv = iota
Bind
BindTry
DevBind
DevBindTry
ROBind
ROBindTry
Chmod
)
var pairArgs = [...]string{
SetEnv: "--setenv",
Bind: "--bind",
BindTry: "--bind-try",
DevBind: "--dev-bind",
DevBindTry: "--dev-bind-try",
ROBind: "--ro-bind",
ROBindTry: "--ro-bind-try",
Chmod: "--chmod",
}
func (c *Config) pairArgs() Builder {
var n pairArg
n[SetEnv] = make([][2]string, len(c.SetEnv))
keys := make([]string, 0, len(c.SetEnv))
for k := range c.SetEnv {
keys = append(keys, k)
}
slices.Sort(keys)
for i, k := range keys {
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
}
// Arg types:
// Bind
// BindTry
// DevBind
// DevBindTry
// ROBind
// ROBindTry
// Chmod
// are handled by the sequential builder
return &n
}
type pairArg [len(pairArgs)][][2]string
func (p *pairArg) Len() (l int) {
for _, v := range p {
l += len(v) * 3
}
return
}
func (p *pairArg) Append(args *[]string) {
for i, arg := range p {
for _, v := range arg {
*args = append(*args, pairArgs[i], v[0], v[1])
}
}
}

View File

@ -1,65 +0,0 @@
package bwrap
const (
Hostname = iota
Chdir
UnsetEnv
LockFile
RemountRO
Procfs
DevTmpfs
Mqueue
)
var stringArgs = [...]string{
Hostname: "--hostname",
Chdir: "--chdir",
UnsetEnv: "--unsetenv",
LockFile: "--lock-file",
RemountRO: "--remount-ro",
Procfs: "--proc",
DevTmpfs: "--dev",
Mqueue: "--mqueue",
}
func (c *Config) stringArgs() Builder {
n := stringArg{
UnsetEnv: c.UnsetEnv,
LockFile: c.LockFile,
}
if c.Hostname != "" {
n[Hostname] = []string{c.Hostname}
}
if c.Chdir != "" {
n[Chdir] = []string{c.Chdir}
}
// Arg types:
// RemountRO
// Procfs
// DevTmpfs
// Mqueue
// are handled by the sequential builder
return &n
}
type stringArg [len(stringArgs)][]string
func (s *stringArg) Len() (l int) {
for _, arg := range s {
l += len(arg) * 2
}
return
}
func (s *stringArg) Append(args *[]string) {
for i, arg := range s {
for _, v := range arg {
*args = append(*args, stringArgs[i], v)
}
}
}

View File

@ -39,46 +39,60 @@ func (c *Config) Bind(src, dest string, opts ...bool) *Config {
if dev { if dev {
if try { if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBindTry], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.Unwrap(), src, dest})
} else { } else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBind], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{DevBind.Unwrap(), src, dest})
} }
return c return c
} else if write { } else if write {
if try { if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[BindTry], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{BindTry.Unwrap(), src, dest})
} else { } else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[Bind], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{Bind.Unwrap(), src, dest})
} }
return c return c
} else { } else {
if try { if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBindTry], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.Unwrap(), src, dest})
} else { } else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBind], src, dest}) c.Filesystem = append(c.Filesystem, &pairF{ROBind.Unwrap(), src, dest})
} }
return c return c
} }
} }
// Dir create dir in sandbox
// (--dir DEST)
func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Dir.Unwrap(), dest})
return c
}
// RemountRO remount path as readonly; does not recursively remount // RemountRO remount path as readonly; does not recursively remount
// (--remount-ro DEST) // (--remount-ro DEST)
func (c *Config) RemountRO(dest string) *Config { func (c *Config) RemountRO(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[RemountRO], dest}) c.Filesystem = append(c.Filesystem, &stringF{RemountRO.Unwrap(), dest})
return c return c
} }
// Procfs mount new procfs in sandbox // Procfs mount new procfs in sandbox
// (--proc DEST) // (--proc DEST)
func (c *Config) Procfs(dest string) *Config { func (c *Config) Procfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Procfs], dest}) c.Filesystem = append(c.Filesystem, &stringF{Procfs.Unwrap(), dest})
return c return c
} }
// DevTmpfs mount new dev in sandbox // DevTmpfs mount new dev in sandbox
// (--dev DEST) // (--dev DEST)
func (c *Config) DevTmpfs(dest string) *Config { func (c *Config) DevTmpfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[DevTmpfs], dest}) c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.Unwrap(), dest})
return c
}
// Mqueue mount new mqueue in sandbox
// (--mqueue DEST)
func (c *Config) Mqueue(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.Unwrap(), dest})
return c return c
} }
@ -121,20 +135,6 @@ func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
return c return c
} }
// Mqueue mount new mqueue in sandbox
// (--mqueue DEST)
func (c *Config) Mqueue(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Mqueue], dest})
return c
}
// Dir create dir in sandbox
// (--dir DEST)
func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{awkwardArgs[Dir], dest})
return c
}
// Symlink create symlink within sandbox // Symlink create symlink within sandbox
// (--symlink SRC DEST) // (--symlink SRC DEST)
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config { func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {

View File

@ -1,17 +1,9 @@
package bwrap package bwrap
import ( import (
"encoding/gob"
"os" "os"
"strconv"
) )
func init() {
gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig]))
gob.Register(new(OverlayConfig))
}
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)
@ -124,153 +116,3 @@ type UnshareConfig struct {
// create new cgroup namespace // create new cgroup namespace
CGroup bool `json:"cgroup"` CGroup bool `json:"cgroup"`
} }
type PermConfig[T FSBuilder] struct {
// set permissions of next argument
// (--perms OCTAL)
Mode *os.FileMode `json:"mode,omitempty"`
// path to get the new permission
// (--bind-data, --file, etc.)
Inner T `json:"path"`
}
func (p *PermConfig[T]) Path() string {
return p.Inner.Path()
}
func (p *PermConfig[T]) Len() int {
if p.Mode != nil {
return p.Inner.Len() + 2
} else {
return p.Inner.Len()
}
}
func (p *PermConfig[T]) Append(args *[]string) {
if p.Mode != nil {
*args = append(*args, intArgs[Perms], strconv.FormatInt(int64(*p.Mode), 8))
}
p.Inner.Append(args)
}
type TmpfsConfig struct {
// set size of tmpfs
// (--size BYTES)
Size int `json:"size,omitempty"`
// mount point of new tmpfs
// (--tmpfs DEST)
Dir string `json:"dir"`
}
func (t *TmpfsConfig) Path() string {
return t.Dir
}
func (t *TmpfsConfig) Len() int {
if t.Size > 0 {
return 4
} else {
return 2
}
}
func (t *TmpfsConfig) Append(args *[]string) {
if t.Size > 0 {
*args = append(*args, intArgs[Size], strconv.Itoa(t.Size))
}
*args = append(*args, awkwardArgs[Tmpfs], t.Dir)
}
type OverlayConfig struct {
/*
read files from SRC in the following overlay
(--overlay-src SRC)
*/
Src []string `json:"src,omitempty"`
/*
mount overlayfs on DEST, with RWSRC as the host path for writes and
WORKDIR an empty directory on the same filesystem as RWSRC
(--overlay RWSRC WORKDIR DEST)
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
(--tmp-overlay DEST)
if either strings are empty, mount overlayfs read-only on DEST
(--ro-overlay DEST)
*/
Persist *[2]string `json:"persist,omitempty"`
/*
--overlay RWSRC WORKDIR DEST
--tmp-overlay DEST
--ro-overlay DEST
*/
Dest string `json:"dest"`
}
func (o *OverlayConfig) Path() string {
return o.Dest
}
func (o *OverlayConfig) Len() int {
// (--tmp-overlay DEST) or (--ro-overlay DEST)
p := 2
// (--overlay RWSRC WORKDIR DEST)
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
p = 4
}
return p + len(o.Src)*2
}
func (o *OverlayConfig) Append(args *[]string) {
// --overlay-src SRC
for _, src := range o.Src {
*args = append(*args, awkwardArgs[OverlaySrc], src)
}
if o.Persist != nil {
if o.Persist[0] != "" && o.Persist[1] != "" {
// --overlay RWSRC WORKDIR
*args = append(*args, awkwardArgs[Overlay], o.Persist[0], o.Persist[1])
} else {
// --ro-overlay
*args = append(*args, awkwardArgs[ROOverlay])
}
} else {
// --tmp-overlay
*args = append(*args, awkwardArgs[TmpOverlay])
}
// DEST
*args = append(*args, o.Dest)
}
type SymlinkConfig [2]string
func (s SymlinkConfig) Path() string {
return s[1]
}
func (s SymlinkConfig) Len() int {
return 3
}
func (s SymlinkConfig) Append(args *[]string) {
*args = append(*args, awkwardArgs[Symlink], s[0], s[1])
}
type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int {
return len(c)
}
func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c {
*args = append(*args, pairArgs[Chmod], strconv.FormatInt(int64(mode), 8), path)
}
}

View File

@ -1,6 +1,7 @@
package bwrap_test package bwrap_test
import ( import (
"os"
"slices" "slices"
"testing" "testing"
@ -13,6 +14,83 @@ func TestConfig_Args(t *testing.T) {
conf *bwrap.Config conf *bwrap.Config
want []string want []string
}{ }{
{
name: "bind",
conf: (new(bwrap.Config)).
Bind("/etc", "/.fortify/etc").
Bind("/etc", "/.fortify/etc", true).
Bind("/run", "/.fortify/run", false, true).
Bind("/sys/devices", "/.fortify/sys/devices", true, true).
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Bind("/etc", "/.fortify/etc")
"--ro-bind", "/etc", "/.fortify/etc",
// Bind("/etc", "/.fortify/etc", true)
"--ro-bind-try", "/etc", "/.fortify/etc",
// Bind("/run", "/.fortify/run", false, true)
"--bind", "/run", "/.fortify/run",
// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
"--bind-try", "/sys/devices", "/.fortify/sys/devices",
// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
},
},
{
name: "dir remount-ro proc dev mqueue",
conf: (new(bwrap.Config)).
Dir("/.fortify").
RemountRO("/home").
Procfs("/proc").
DevTmpfs("/dev").
Mqueue("/dev/mqueue"),
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Dir("/.fortify")
"--dir", "/.fortify",
// RemountRO("/home")
"--remount-ro", "/home",
// Procfs("/proc")
"--proc", "/proc",
// DevTmpfs("/dev")
"--dev", "/dev",
// Mqueue("/dev/mqueue")
"--mqueue", "/dev/mqueue",
},
},
{
name: "tmpfs",
conf: (new(bwrap.Config)).
Tmpfs("/run/user", 8192).
Tmpfs("/run/dbus", 8192, 0755),
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Tmpfs("/run/user", 8192)
"--size", "8192", "--tmpfs", "/run/user",
// Tmpfs("/run/dbus", 8192, 0755)
"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
},
},
{
name: "symlink",
conf: (new(bwrap.Config)).
Symlink("/.fortify/sbin/init", "/sbin/init").
Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Symlink("/.fortify/sbin/init", "/sbin/init")
"--symlink", "/.fortify/sbin/init", "/sbin/init",
// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
},
},
{ {
name: "overlayfs", name: "overlayfs",
conf: (new(bwrap.Config)). conf: (new(bwrap.Config)).
@ -32,6 +110,64 @@ func TestConfig_Args(t *testing.T) {
"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix", "--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
}, },
}, },
{
name: "unshare",
conf: &bwrap.Config{Unshare: &bwrap.UnshareConfig{
User: false,
IPC: false,
PID: false,
Net: false,
UTS: false,
CGroup: false,
}},
want: []string{"--disable-userns", "--assert-userns-disabled"},
},
{
name: "uid gid sync",
conf: (new(bwrap.Config)).
SetUID(1971).
SetGID(100).
SetSync(os.Stdin),
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// SetUID(1971)
"--uid", "1971",
// SetGID(100)
"--gid", "100",
// SetSync(os.Stdin)
// this is set when the process is created
},
},
{
name: "hostname chdir setenv unsetenv lockfile chmod",
conf: &bwrap.Config{
Hostname: "fortify",
Chdir: "/.fortify",
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
UnsetEnv: []string{"HOME", "HOST"},
LockFile: []string{"/.fortify/lock"},
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
},
want: []string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Hostname: "fortify"
"--hostname", "fortify",
// Chdir: "/.fortify"
"--chdir", "/.fortify",
// UnsetEnv: []string{"HOME", "HOST"}
"--unsetenv", "HOME",
"--unsetenv", "HOST",
// LockFile: []string{"/.fortify/lock"},
"--lock-file", "/.fortify/lock",
// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
"--chmod", "755", "/.fortify/sbin/init",
},
},
{ {
name: "xdg-dbus-proxy constraint sample", name: "xdg-dbus-proxy constraint sample",
conf: (&bwrap.Config{ conf: (&bwrap.Config{
@ -90,148 +226,6 @@ func TestConfig_Args(t *testing.T) {
"--ro-bind", "/etc", "/etc", "--ro-bind", "/etc", "/etc",
}, },
}, },
{
name: "fortify permissive default nixos",
conf: (&bwrap.Config{
Unshare: nil,
Net: true,
UserNS: true,
Clearenv: true,
SetEnv: map[string]string{
"HOME": "/home/chronos",
"TERM": "xterm-256color",
"FORTIFY_INIT": "3",
"XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
"SHELL": "/run/current-system/sw/bin/zsh",
"USER": "chronos",
},
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/etc", "/etc", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608).
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
want: []string{
"--unshare-all", "--unshare-user", "--share-net",
"--clearenv", "--die-with-parent", "--as-pid-1",
"--uid", "65534",
"--gid", "65534",
"--setenv", "FORTIFY_INIT", "3",
"--setenv", "HOME", "/home/chronos",
"--setenv", "SHELL", "/run/current-system/sw/bin/zsh",
"--setenv", "TERM", "xterm-256color",
"--setenv", "USER", "chronos",
"--setenv", "XDG_RUNTIME_DIR", "/run/user/150",
"--setenv", "XDG_SESSION_CLASS", "user",
"--setenv", "XDG_SESSION_TYPE", "tty",
"--proc", "/proc", "--dev", "/dev",
"--mqueue", "/dev/mqueue",
"--bind", "/bin", "/bin",
"--bind", "/boot", "/boot",
"--bind", "/etc", "/etc",
"--bind", "/home", "/home",
"--bind", "/lib", "/lib",
"--bind", "/lib64", "/lib64",
"--bind", "/nix", "/nix",
"--bind", "/root", "/root",
"--bind", "/srv", "/srv",
"--bind", "/sys", "/sys",
"--bind", "/usr", "/usr",
"--bind", "/var", "/var",
"--bind", "/run/NetworkManager", "/run/NetworkManager",
"--bind", "/run/agetty.reload", "/run/agetty.reload",
"--bind", "/run/binfmt", "/run/binfmt",
"--bind", "/run/booted-system", "/run/booted-system",
"--bind", "/run/credentials", "/run/credentials",
"--bind", "/run/cryptsetup", "/run/cryptsetup",
"--bind", "/run/current-system", "/run/current-system",
"--bind", "/run/host", "/run/host",
"--bind", "/run/keys", "/run/keys",
"--bind", "/run/libvirt", "/run/libvirt",
"--bind", "/run/libvirtd.pid", "/run/libvirtd.pid",
"--bind", "/run/lock", "/run/lock",
"--bind", "/run/log", "/run/log",
"--bind", "/run/lvm", "/run/lvm",
"--bind", "/run/mount", "/run/mount",
"--bind", "/run/nginx", "/run/nginx",
"--bind", "/run/nscd", "/run/nscd",
"--bind", "/run/opengl-driver", "/run/opengl-driver",
"--bind", "/run/pppd", "/run/pppd",
"--bind", "/run/resolvconf", "/run/resolvconf",
"--bind", "/run/sddm", "/run/sddm",
"--bind", "/run/syncoid", "/run/syncoid",
"--bind", "/run/systemd", "/run/systemd",
"--bind", "/run/tmpfiles.d", "/run/tmpfiles.d",
"--bind", "/run/udev", "/run/udev",
"--bind", "/run/udisks2", "/run/udisks2",
"--bind", "/run/utmp", "/run/utmp",
"--bind", "/run/virtlogd.pid", "/run/virtlogd.pid",
"--bind", "/run/wrappers", "/run/wrappers",
"--bind", "/run/zed.pid", "/run/zed.pid",
"--bind", "/run/zed.state", "/run/zed.state",
"--bind", "/tmp/fortify.1971/tmpdir/150", "/tmp",
"--size", "1048576", "--tmpfs", "/tmp/fortify.1971",
"--size", "1048576", "--tmpfs", "/run/user",
"--size", "8388608", "--tmpfs", "/run/user/150",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group",
"--size", "8192", "--tmpfs", "/var/run/nscd",
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -241,4 +235,21 @@ func TestConfig_Args(t *testing.T) {
} }
}) })
} }
// test persist validation
t.Run("invalid persist", func(t *testing.T) {
defer func() {
wantPanic := "persist called without required paths"
if r := recover(); r != wantPanic {
t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
}
}()
(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)
}
})
} }

223
helper/bwrap/sequential.go Normal file
View File

@ -0,0 +1,223 @@
package bwrap
import (
"encoding/gob"
"os"
"strconv"
)
func init() {
gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig]))
gob.Register(new(OverlayConfig))
}
type PositionalArg int
func (p PositionalArg) Unwrap() string {
return positionalArgs[p]
}
const (
Tmpfs PositionalArg = iota
Symlink
Bind
BindTry
DevBind
DevBindTry
ROBind
ROBindTry
Chmod
Dir
RemountRO
Procfs
DevTmpfs
Mqueue
Perms
Size
OverlaySrc
Overlay
TmpOverlay
ROOverlay
)
var positionalArgs = [...]string{
Tmpfs: "--tmpfs",
Symlink: "--symlink",
Bind: "--bind",
BindTry: "--bind-try",
DevBind: "--dev-bind",
DevBindTry: "--dev-bind-try",
ROBind: "--ro-bind",
ROBindTry: "--ro-bind-try",
Chmod: "--chmod",
Dir: "--dir",
RemountRO: "--remount-ro",
Procfs: "--proc",
DevTmpfs: "--dev",
Mqueue: "--mqueue",
Perms: "--perms",
Size: "--size",
OverlaySrc: "--overlay-src",
Overlay: "--overlay",
TmpOverlay: "--tmp-overlay",
ROOverlay: "--ro-overlay",
}
type PermConfig[T FSBuilder] struct {
// set permissions of next argument
// (--perms OCTAL)
Mode *os.FileMode `json:"mode,omitempty"`
// path to get the new permission
// (--bind-data, --file, etc.)
Inner T `json:"path"`
}
func (p *PermConfig[T]) Path() string {
return p.Inner.Path()
}
func (p *PermConfig[T]) Len() int {
if p.Mode != nil {
return p.Inner.Len() + 2
} else {
return p.Inner.Len()
}
}
func (p *PermConfig[T]) Append(args *[]string) {
if p.Mode != nil {
*args = append(*args, Perms.Unwrap(), strconv.FormatInt(int64(*p.Mode), 8))
}
p.Inner.Append(args)
}
type TmpfsConfig struct {
// set size of tmpfs
// (--size BYTES)
Size int `json:"size,omitempty"`
// mount point of new tmpfs
// (--tmpfs DEST)
Dir string `json:"dir"`
}
func (t *TmpfsConfig) Path() string {
return t.Dir
}
func (t *TmpfsConfig) Len() int {
if t.Size > 0 {
return 4
} else {
return 2
}
}
func (t *TmpfsConfig) Append(args *[]string) {
if t.Size > 0 {
*args = append(*args, Size.Unwrap(), strconv.Itoa(t.Size))
}
*args = append(*args, Tmpfs.Unwrap(), t.Dir)
}
type OverlayConfig struct {
/*
read files from SRC in the following overlay
(--overlay-src SRC)
*/
Src []string `json:"src,omitempty"`
/*
mount overlayfs on DEST, with RWSRC as the host path for writes and
WORKDIR an empty directory on the same filesystem as RWSRC
(--overlay RWSRC WORKDIR DEST)
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
(--tmp-overlay DEST)
if either strings are empty, mount overlayfs read-only on DEST
(--ro-overlay DEST)
*/
Persist *[2]string `json:"persist,omitempty"`
/*
--overlay RWSRC WORKDIR DEST
--tmp-overlay DEST
--ro-overlay DEST
*/
Dest string `json:"dest"`
}
func (o *OverlayConfig) Path() string {
return o.Dest
}
func (o *OverlayConfig) Len() int {
// (--tmp-overlay DEST) or (--ro-overlay DEST)
p := 2
// (--overlay RWSRC WORKDIR DEST)
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
p = 4
}
return p + len(o.Src)*2
}
func (o *OverlayConfig) Append(args *[]string) {
// --overlay-src SRC
for _, src := range o.Src {
*args = append(*args, OverlaySrc.Unwrap(), src)
}
if o.Persist != nil {
if o.Persist[0] != "" && o.Persist[1] != "" {
// --overlay RWSRC WORKDIR
*args = append(*args, Overlay.Unwrap(), o.Persist[0], o.Persist[1])
} else {
// --ro-overlay
*args = append(*args, ROOverlay.Unwrap())
}
} else {
// --tmp-overlay
*args = append(*args, TmpOverlay.Unwrap())
}
// DEST
*args = append(*args, o.Dest)
}
type SymlinkConfig [2]string
func (s SymlinkConfig) Path() string {
return s[1]
}
func (s SymlinkConfig) Len() int {
return 3
}
func (s SymlinkConfig) Append(args *[]string) {
*args = append(*args, Symlink.Unwrap(), s[0], s[1])
}
type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int {
return len(c)
}
func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c {
*args = append(*args, Chmod.Unwrap(), strconv.FormatInt(int64(mode), 8), path)
}
}

249
helper/bwrap/static.go Normal file
View File

@ -0,0 +1,249 @@
package bwrap
import (
"slices"
"strconv"
)
/*
static boolean args
*/
type BoolArg int
func (b BoolArg) Unwrap() []string {
return boolArgs[b]
}
const (
UnshareAll BoolArg = iota
UnshareUser
UnshareIPC
UnsharePID
UnshareNet
UnshareUTS
UnshareCGroup
ShareNet
UserNS
Clearenv
NewSession
DieWithParent
AsInit
)
var boolArgs = [...][]string{
UnshareAll: {"--unshare-all", "--unshare-user"},
UnshareUser: {"--unshare-user"},
UnshareIPC: {"--unshare-ipc"},
UnsharePID: {"--unshare-pid"},
UnshareNet: {"--unshare-net"},
UnshareUTS: {"--unshare-uts"},
UnshareCGroup: {"--unshare-cgroup"},
ShareNet: {"--share-net"},
UserNS: {"--disable-userns", "--assert-userns-disabled"},
Clearenv: {"--clearenv"},
NewSession: {"--new-session"},
DieWithParent: {"--die-with-parent"},
AsInit: {"--as-pid-1"},
}
func (c *Config) boolArgs() Builder {
b := boolArg{
UserNS: !c.UserNS,
Clearenv: c.Clearenv,
NewSession: c.NewSession,
DieWithParent: c.DieWithParent,
AsInit: c.AsInit,
}
if c.Unshare == nil {
b[UnshareAll] = true
b[ShareNet] = c.Net
} else {
b[UnshareUser] = c.Unshare.User
b[UnshareIPC] = c.Unshare.IPC
b[UnsharePID] = c.Unshare.PID
b[UnshareNet] = c.Unshare.Net
b[UnshareUTS] = c.Unshare.UTS
b[UnshareCGroup] = c.Unshare.CGroup
}
return &b
}
type boolArg [len(boolArgs)]bool
func (b *boolArg) Len() (l int) {
for i, v := range b {
if v {
l += len(boolArgs[i])
}
}
return
}
func (b *boolArg) Append(args *[]string) {
for i, v := range b {
if v {
*args = append(*args, BoolArg(i).Unwrap()...)
}
}
}
/*
static integer args
*/
type IntArg int
func (i IntArg) Unwrap() string {
return intArgs[i]
}
const (
UID IntArg = iota
GID
)
var intArgs = [...]string{
UID: "--uid",
GID: "--gid",
}
func (c *Config) intArgs() Builder {
return &intArg{
UID: c.UID,
GID: c.GID,
}
}
type intArg [len(intArgs)]*int
func (n *intArg) Len() (l int) {
for _, v := range n {
if v != nil {
l += 2
}
}
return
}
func (n *intArg) Append(args *[]string) {
for i, v := range n {
if v != nil {
*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
}
}
}
/*
static string args
*/
type StringArg int
func (s StringArg) Unwrap() string {
return stringArgs[s]
}
const (
Hostname StringArg = iota
Chdir
UnsetEnv
LockFile
)
var stringArgs = [...]string{
Hostname: "--hostname",
Chdir: "--chdir",
UnsetEnv: "--unsetenv",
LockFile: "--lock-file",
}
func (c *Config) stringArgs() Builder {
n := stringArg{
UnsetEnv: c.UnsetEnv,
LockFile: c.LockFile,
}
if c.Hostname != "" {
n[Hostname] = []string{c.Hostname}
}
if c.Chdir != "" {
n[Chdir] = []string{c.Chdir}
}
return &n
}
type stringArg [len(stringArgs)][]string
func (s *stringArg) Len() (l int) {
for _, arg := range s {
l += len(arg) * 2
}
return
}
func (s *stringArg) Append(args *[]string) {
for i, arg := range s {
for _, v := range arg {
*args = append(*args, StringArg(i).Unwrap(), v)
}
}
}
/*
static pair args
*/
type PairArg int
func (p PairArg) Unwrap() string {
return pairArgs[p]
}
const (
SetEnv PairArg = iota
)
var pairArgs = [...]string{
SetEnv: "--setenv",
}
func (c *Config) pairArgs() Builder {
var n pairArg
n[SetEnv] = make([][2]string, len(c.SetEnv))
keys := make([]string, 0, len(c.SetEnv))
for k := range c.SetEnv {
keys = append(keys, k)
}
slices.Sort(keys)
for i, k := range keys {
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
}
return &n
}
type pairArg [len(pairArgs)][][2]string
func (p *pairArg) Len() (l int) {
for _, v := range p {
l += len(v) * 3
}
return
}
func (p *pairArg) Append(args *[]string) {
for i, arg := range p {
for _, v := range arg {
*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
}
}
}