helper/bwrap: implement file copy flags
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Run NixOS test (push) Successful in 3m44s

These are significantly more efficient and less error-prone than mounting an external tmpfile. This should also reduce attack surface as the resulting files are private to its specific sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-02-15 03:12:28 +09:00
parent ea8d1c07df
commit c04ca0f5fa
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 181 additions and 92 deletions

View File

@ -1,6 +1,8 @@
package bwrap package bwrap
import "os" import (
"os"
)
/* /*
Bind binds mount src on host to dest in sandbox. Bind binds mount src on host to dest in sandbox.
@ -61,6 +63,29 @@ func (c *Config) Bind(src, dest string, opts ...bool) *Config {
} }
} }
// Write copy from FD to destination DEST
// (--file FD DEST)
func (c *Config) Write(dest string, payload []byte) *Config {
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, Type: DataWrite})
return c
}
/*
CopyBind copy from FD to file which is readonly bind-mounted on DEST
(--ro-bind-data FD DEST)
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
(--bind-data FD DEST)
*/
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
t := DataROBind
if len(opts) > 0 && opts[0] {
t = DataBind
}
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, Type: t})
return c
}
// Dir create dir in sandbox // Dir create dir in sandbox
// (--dir DEST) // (--dir DEST)
func (c *Config) Dir(dest string) *Config { func (c *Config) Dir(dest string) *Config {

View File

@ -71,9 +71,6 @@ type Config struct {
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST --ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
--exec-label LABEL Exec label for the sandbox --exec-label LABEL Exec label for the sandbox
--file-label LABEL File label for temporary sandbox content --file-label LABEL File label for temporary sandbox content
--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
--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

View File

@ -21,15 +21,14 @@ func TestConfig_Args(t *testing.T) {
want []string want []string
}{ }{
{ {
name: "bind", "bind", (new(bwrap.Config)).
conf: (new(bwrap.Config)). Bind("/etc", "/.fortify/etc").
Bind("/etc", "/.fortify/etc"). Bind("/etc", "/.fortify/etc", true).
Bind("/etc", "/.fortify/etc", true). Bind("/run", "/.fortify/run", false, true).
Bind("/run", "/.fortify/run", false, true). Bind("/sys/devices", "/.fortify/sys/devices", true, true).
Bind("/sys/devices", "/.fortify/sys/devices", true, true). Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true). Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Bind("/etc", "/.fortify/etc") // Bind("/etc", "/.fortify/etc")
@ -47,14 +46,13 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "dir remount-ro proc dev mqueue", "dir remount-ro proc dev mqueue", (new(bwrap.Config)).
conf: (new(bwrap.Config)). Dir("/.fortify").
Dir("/.fortify"). RemountRO("/home").
RemountRO("/home"). Procfs("/proc").
Procfs("/proc"). DevTmpfs("/dev").
DevTmpfs("/dev"). Mqueue("/dev/mqueue"),
Mqueue("/dev/mqueue"), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Dir("/.fortify") // Dir("/.fortify")
@ -70,11 +68,10 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "tmpfs", "tmpfs", (new(bwrap.Config)).
conf: (new(bwrap.Config)). Tmpfs("/run/user", 8192).
Tmpfs("/run/user", 8192). Tmpfs("/run/dbus", 8192, 0755),
Tmpfs("/run/dbus", 8192, 0755), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Tmpfs("/run/user", 8192) // Tmpfs("/run/user", 8192)
@ -84,11 +81,10 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "symlink", "symlink", (new(bwrap.Config)).
conf: (new(bwrap.Config)). Symlink("/.fortify/sbin/init", "/sbin/init").
Symlink("/.fortify/sbin/init", "/sbin/init"). Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
Symlink("/.fortify/sbin/init", "/sbin/init", 0755), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Symlink("/.fortify/sbin/init", "/sbin/init") // Symlink("/.fortify/sbin/init", "/sbin/init")
@ -98,12 +94,11 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "overlayfs", "overlayfs", (new(bwrap.Config)).
conf: (new(bwrap.Config)). Overlay("/etc", "/etc").
Overlay("/etc", "/etc"). Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin"). Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Overlay("/etc", "/etc") // Overlay("/etc", "/etc")
@ -117,23 +112,37 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "unshare", "copy", (new(bwrap.Config)).
conf: &bwrap.Config{Unshare: &bwrap.UnshareConfig{ Write("/.fortify/version", make([]byte, 8)).
User: false, CopyBind("/etc/group", make([]byte, 8)).
IPC: false, CopyBind("/etc/passwd", make([]byte, 8), true),
PID: false, []string{
Net: false, "--unshare-all", "--unshare-user",
UTS: false, "--disable-userns", "--assert-userns-disabled",
CGroup: false, // Write("/.fortify/version", make([]byte, 8))
}}, "--file", "3", "/.fortify/version",
want: []string{"--disable-userns", "--assert-userns-disabled"}, // CopyBind("/etc/group", make([]byte, 8))
"--ro-bind-data", "4", "/etc/group",
// CopyBind("/etc/passwd", make([]byte, 8), true)
"--bind-data", "5", "/etc/passwd",
},
}, },
{ {
name: "uid gid sync", "unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
conf: (new(bwrap.Config)). User: false,
SetUID(1971). IPC: false,
SetGID(100), PID: false,
want: []string{ Net: false,
UTS: false,
CGroup: false,
}},
[]string{"--disable-userns", "--assert-userns-disabled"},
},
{
"uid gid sync", (new(bwrap.Config)).
SetUID(1971).
SetGID(100),
[]string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// SetUID(1971) // SetUID(1971)
@ -143,17 +152,16 @@ func TestConfig_Args(t *testing.T) {
}, },
}, },
{ {
name: "hostname chdir setenv unsetenv lockfile chmod syscall", "hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
conf: &bwrap.Config{ Hostname: "fortify",
Hostname: "fortify", Chdir: "/.fortify",
Chdir: "/.fortify", SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}, UnsetEnv: []string{"HOME", "HOST"},
UnsetEnv: []string{"HOME", "HOST"}, LockFile: []string{"/.fortify/lock"},
LockFile: []string{"/.fortify/lock"}, Syscall: new(bwrap.SyscallPolicy),
Syscall: new(bwrap.SyscallPolicy), Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}, },
}, []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
// Hostname: "fortify" // Hostname: "fortify"
@ -175,31 +183,30 @@ func TestConfig_Args(t *testing.T) {
}, },
{ {
name: "xdg-dbus-proxy constraint sample", "xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
conf: (&bwrap.Config{Clearenv: true, DieWithParent: true}). Symlink("usr/bin", "/bin").
Symlink("usr/bin", "/bin"). Symlink("var/home", "/home").
Symlink("var/home", "/home"). Symlink("usr/lib", "/lib").
Symlink("usr/lib", "/lib"). Symlink("usr/lib64", "/lib64").
Symlink("usr/lib64", "/lib64"). Symlink("run/media", "/media").
Symlink("run/media", "/media"). Symlink("var/mnt", "/mnt").
Symlink("var/mnt", "/mnt"). Symlink("var/opt", "/opt").
Symlink("var/opt", "/opt"). Symlink("sysroot/ostree", "/ostree").
Symlink("sysroot/ostree", "/ostree"). Symlink("var/roothome", "/root").
Symlink("var/roothome", "/root"). Symlink("usr/sbin", "/sbin").
Symlink("usr/sbin", "/sbin"). Symlink("var/srv", "/srv").
Symlink("var/srv", "/srv"). Bind("/run", "/run", false, true).
Bind("/run", "/run", false, true). Bind("/tmp", "/tmp", false, true).
Bind("/tmp", "/tmp", false, true). Bind("/var", "/var", false, true).
Bind("/var", "/var", false, true). Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true). Bind("/boot", "/boot").
Bind("/boot", "/boot"). Bind("/dev", "/dev").
Bind("/dev", "/dev"). Bind("/proc", "/proc").
Bind("/proc", "/proc"). Bind("/sys", "/sys").
Bind("/sys", "/sys"). Bind("/sysroot", "/sysroot").
Bind("/sysroot", "/sysroot"). Bind("/usr", "/usr").
Bind("/usr", "/usr"). Bind("/etc", "/etc"),
Bind("/etc", "/etc"), []string{
want: []string{
"--unshare-all", "--unshare-user", "--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled", "--disable-userns", "--assert-userns-disabled",
"--clearenv", "--die-with-parent", "--clearenv", "--die-with-parent",

View File

@ -2,14 +2,19 @@ package bwrap
import ( import (
"encoding/gob" "encoding/gob"
"fmt"
"io"
"os" "os"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
) )
func init() { func init() {
gob.Register(new(PermConfig[SymlinkConfig])) gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig])) gob.Register(new(PermConfig[*TmpfsConfig]))
gob.Register(new(OverlayConfig)) gob.Register(new(OverlayConfig))
gob.Register(new(DataConfig))
} }
type PositionalArg int type PositionalArg int
@ -44,6 +49,10 @@ const (
SyncFd SyncFd
Seccomp Seccomp
File
BindData
ROBindData
) )
var positionalArgs = [...]string{ var positionalArgs = [...]string{
@ -74,6 +83,10 @@ var positionalArgs = [...]string{
SyncFd: "--sync-fd", SyncFd: "--sync-fd",
Seccomp: "--seccomp", Seccomp: "--seccomp",
File: "--file",
BindData: "--bind-data",
ROBindData: "--ro-bind-data",
} }
type PermConfig[T FSBuilder] struct { type PermConfig[T FSBuilder] struct {
@ -202,12 +215,59 @@ func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.St
type ChmodConfig map[string]os.FileMode type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int { func (c ChmodConfig) Len() int { return len(c) }
return len(c)
}
func (c ChmodConfig) Append(args *[]string) { func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c { for path, mode := range c {
*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path) *args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
} }
} }
const (
DataWrite = iota
DataBind
DataROBind
)
type DataConfig struct {
Dest string `json:"dest"`
Data []byte `json:"data,omitempty"`
Type int `json:"type"`
proc.File
}
func (d *DataConfig) Path() string { return d.Dest }
func (d *DataConfig) Len() int {
if d == nil || d.Data == nil {
return 0
}
return 3
}
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
if d.File != nil {
panic("file initialised twice")
}
d.File = proc.NewWriterTo(d)
return d.File.Init(fd, v)
}
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(d.Data)
return int64(n), err
}
func (d *DataConfig) Append(args *[]string) {
if d == nil || d.Data == nil {
return
}
var a PositionalArg
switch d.Type {
case DataWrite:
a = File
case DataBind:
a = BindData
case DataROBind:
a = ROBindData
default:
panic(fmt.Sprintf("invalid type %d", a))
}
*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
}