diff --git a/flake.nix b/flake.nix index 29a6367..1d251ab 100644 --- a/flake.nix +++ b/flake.nix @@ -116,7 +116,6 @@ # appPackages glibc - bubblewrap xdg-dbus-proxy # fpkg diff --git a/helper/bwrap.go b/helper/bwrap.go deleted file mode 100644 index 97eacf8..0000000 --- a/helper/bwrap.go +++ /dev/null @@ -1,72 +0,0 @@ -package helper - -import ( - "context" - "io" - "os" - "os/exec" - "slices" - "strconv" - - "git.gensokyo.uk/security/fortify/helper/bwrap" - "git.gensokyo.uk/security/fortify/helper/proc" -) - -// BubblewrapName is the file name or path to bubblewrap. -var BubblewrapName = "bwrap" - -// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer. -// If wt is nil, the child process spawned by bwrap will not get an argument pipe. -// Function argF returns an array of arguments passed directly to the child process. -func MustNewBwrap( - ctx context.Context, - name string, - wt io.WriterTo, - stat bool, - argF func(argsFd, statFd int) []string, - cmdF func(cmd *exec.Cmd), - extraFiles []*os.File, - conf *bwrap.Config, - syncFd *os.File, -) Helper { - b, err := NewBwrap(ctx, name, wt, stat, argF, cmdF, extraFiles, conf, syncFd) - if err != nil { - panic(err.Error()) - } else { - return b - } -} - -// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer. -// If wt is nil, the child process spawned by bwrap will not get an argument pipe. -// Function argF returns an array of arguments passed directly to the child process. -func NewBwrap( - ctx context.Context, - name string, - wt io.WriterTo, - stat bool, - argF func(argsFd, statFd int) []string, - cmdF func(cmd *exec.Cmd), - extraFiles []*os.File, - conf *bwrap.Config, - syncFd *os.File, -) (Helper, error) { - b, args := newHelperCmd(ctx, BubblewrapName, wt, stat, argF, extraFiles) - - var argsFd uintptr - if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil { - return nil, err - } else { - f := proc.NewWriterTo(v) - argsFd = proc.InitFile(f, b.extraFiles) - b.files = append(b.files, f) - } - - b.Args = slices.Grow(b.Args, 4+len(args)) - b.Args = append(b.Args, "--args", strconv.Itoa(int(argsFd)), "--", name) - b.Args = append(b.Args, args...) - if cmdF != nil { - cmdF(b.Cmd) - } - return b, nil -} diff --git a/helper/bwrap/arg.go b/helper/bwrap/arg.go deleted file mode 100644 index 6d79a8b..0000000 --- a/helper/bwrap/arg.go +++ /dev/null @@ -1,72 +0,0 @@ -package bwrap - -import ( - "os" - "slices" - - "git.gensokyo.uk/security/fortify/helper/proc" -) - -type Builder interface { - Len() int - Append(args *[]string) -} - -type FSBuilder interface { - Path() string - Builder -} - -type FDBuilder interface { - proc.File - Builder -} - -// Args returns a slice of bwrap args corresponding to c. -func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) { - builders := []Builder{ - c.boolArgs(), - c.intArgs(), - c.stringArgs(), - c.pairArgs(), - c.seccompArgs(), - newFile(SyncFd.String(), syncFd), - } - - builders = slices.Grow(builders, len(c.Filesystem)+1) - for _, f := range c.Filesystem { - builders = append(builders, f) - } - builders = append(builders, c.Chmod) - - argc := 0 - fc := 0 - for _, b := range builders { - l := b.Len() - if l < 1 { - continue - } - argc += l - - if f, ok := b.(FDBuilder); ok { - fc++ - proc.InitFile(f, extraFiles) - } - } - fc++ // allocate extra slot for stat fd - - args = make([]string, 0, argc) - *files = slices.Grow(*files, fc) - for _, b := range builders { - if b.Len() < 1 { - continue - } - b.Append(&args) - - if f, ok := b.(FDBuilder); ok { - *files = append(*files, f) - } - } - - return -} diff --git a/helper/bwrap/builder.go b/helper/bwrap/builder.go deleted file mode 100644 index a948192..0000000 --- a/helper/bwrap/builder.go +++ /dev/null @@ -1,199 +0,0 @@ -package bwrap - -import ( - "os" -) - -/* -Bind binds mount src on host to dest in sandbox. - -Bind(src, dest) bind mount host path readonly on sandbox -(--ro-bind SRC DEST). -Bind(src, dest, true) equal to ROBind but ignores non-existent host path -(--ro-bind-try SRC DEST). - -Bind(src, dest, false, true) bind mount host path on sandbox. -(--bind SRC DEST). -Bind(src, dest, true, true) equal to Bind but ignores non-existent host path -(--bind-try SRC DEST). - -Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access -(--dev-bind SRC DEST). -Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path -(--dev-bind-try SRC DEST). -*/ -func (c *Config) Bind(src, dest string, opts ...bool) *Config { - var ( - try bool - write bool - dev bool - ) - - if len(opts) > 0 { - try = opts[0] - } - if len(opts) > 1 { - write = opts[1] - } - if len(opts) > 2 { - dev = opts[2] - } - - if dev { - if try { - c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest}) - } else { - c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest}) - } - return c - } else if write { - if try { - c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest}) - } else { - c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest}) - } - return c - } else { - if try { - c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest}) - } else { - c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest}) - } - return c - } -} - -// WriteFile copy from FD to destination DEST -// (--file FD DEST) -func (c *Config) WriteFile(name string, data []byte) *Config { - c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, 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 { - var p *[]byte - c.CopyBindRef(dest, &p, opts...) - *p = payload - return c -} - -// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data. -func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config { - t := DataROBind - if len(opts) > 0 && opts[0] { - t = DataBind - } - d := &DataConfig{Dest: dest, Type: t} - *payloadRef = &d.Data - - c.Filesystem = append(c.Filesystem, d) - return c -} - -// Dir create dir in sandbox -// (--dir DEST) -func (c *Config) Dir(dest string) *Config { - c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest}) - return c -} - -// RemountRO remount path as readonly; does not recursively remount -// (--remount-ro DEST) -func (c *Config) RemountRO(dest string) *Config { - c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest}) - return c -} - -// Procfs mount new procfs in sandbox -// (--proc DEST) -func (c *Config) Procfs(dest string) *Config { - c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest}) - return c -} - -// DevTmpfs mount new dev in sandbox -// (--dev DEST) -func (c *Config) DevTmpfs(dest string) *Config { - c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), 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.String(), dest}) - return c -} - -// Tmpfs mount new tmpfs in sandbox -// (--tmpfs DEST) -func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config { - tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}} - if size >= 0 { - tmpfs.Inner.Size = size - } - if len(perm) == 1 { - tmpfs.Mode = &perm[0] - } - c.Filesystem = append(c.Filesystem, tmpfs) - return c -} - -// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs -// (--tmp-overlay DEST) -func (c *Config) Overlay(dest string, src ...string) *Config { - c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest}) - return c -} - -// Join mount overlayfs read-only on DEST -// (--ro-overlay DEST) -func (c *Config) Join(dest string, src ...string) *Config { - c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)}) - return c -} - -// Persist 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) -func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config { - if rwsrc == "" || workdir == "" { - panic("persist called without required paths") - } - c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}}) - return c -} - -// Symlink create symlink within sandbox -// (--symlink SRC DEST) -func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config { - symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}} - if len(perm) == 1 { - symlink.Mode = &perm[0] - } - c.Filesystem = append(c.Filesystem, symlink) - return c -} - -// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID). -func (c *Config) SetUID(uid int) *Config { - if uid >= 0 { - c.UID = &uid - } - return c -} - -// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID). -func (c *Config) SetGID(gid int) *Config { - if gid >= 0 { - c.GID = &gid - } - return c -} diff --git a/helper/bwrap/config.go b/helper/bwrap/config.go deleted file mode 100644 index fdda14a..0000000 --- a/helper/bwrap/config.go +++ /dev/null @@ -1,104 +0,0 @@ -package bwrap - -type Config struct { - // unshare every namespace we support by default if nil - // (--unshare-all) - Unshare *UnshareConfig `json:"unshare,omitempty"` - // retain the network namespace (can only combine with nil Unshare) - // (--share-net) - Net bool `json:"net"` - - // disable further use of user namespaces inside sandbox and fail unless - // further use of user namespace inside sandbox is disabled if false - // (--disable-userns) (--assert-userns-disabled) - UserNS bool `json:"userns"` - - // custom uid in the sandbox, requires new user namespace - // (--uid UID) - UID *int `json:"uid,omitempty"` - // custom gid in the sandbox, requires new user namespace - // (--gid GID) - GID *int `json:"gid,omitempty"` - // custom hostname in the sandbox, requires new uts namespace - // (--hostname NAME) - Hostname string `json:"hostname,omitempty"` - - // change directory - // (--chdir DIR) - Chdir string `json:"chdir,omitempty"` - // unset all environment variables - // (--clearenv) - Clearenv bool `json:"clearenv"` - // set environment variable - // (--setenv VAR VALUE) - SetEnv map[string]string `json:"setenv,omitempty"` - // unset environment variables - // (--unsetenv VAR) - UnsetEnv []string `json:"unsetenv,omitempty"` - - // take a lock on file while sandbox is running - // (--lock-file DEST) - LockFile []string `json:"lock_file,omitempty"` - - // ordered filesystem args - Filesystem []FSBuilder `json:"filesystem,omitempty"` - - // change permissions (must already exist) - // (--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"` - // kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies. - // (--die-with-parent) - DieWithParent bool `json:"die_with_parent"` - // do not install a reaper process with PID=1 - // (--as-pid-1) - AsInit bool `json:"as_init"` - - /* unmapped options include: - --unshare-user-try Create new user namespace if possible else continue by skipping it - --unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it - --userns FD Use this user namespace (cannot combine with --unshare-user) - --userns2 FD After setup switch to this user namespace, only useful with --userns - --pidns FD Use this pid namespace (as parent namespace if using --unshare-pid) - --bind-fd FD DEST Bind open directory or path fd 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 - --file-label LABEL File label for temporary sandbox content - --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 - --info-fd FD Write information about the running container to FD - --json-status-fd FD Write container status to FD as multiple JSON documents - --cap-add CAP Add cap CAP when running as privileged user - --cap-drop CAP Drop cap CAP when running as privileged user - - among which --args is used internally for passing arguments */ -} - -type UnshareConfig struct { - // (--unshare-user) - // create new user namespace - User bool `json:"user"` - // (--unshare-ipc) - // create new ipc namespace - IPC bool `json:"ipc"` - // (--unshare-pid) - // create new pid namespace - PID bool `json:"pid"` - // (--unshare-net) - // create new network namespace - Net bool `json:"net"` - // (--unshare-uts) - // create new uts namespace - UTS bool `json:"uts"` - // (--unshare-cgroup) - // create new cgroup namespace - CGroup bool `json:"cgroup"` -} diff --git a/helper/bwrap/config_test.go b/helper/bwrap/config_test.go deleted file mode 100644 index 720ef40..0000000 --- a/helper/bwrap/config_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package bwrap_test - -import ( - "os" - "slices" - "testing" - - "git.gensokyo.uk/security/fortify/helper/bwrap" - "git.gensokyo.uk/security/fortify/helper/proc" - "git.gensokyo.uk/security/fortify/sandbox/seccomp" -) - -func TestConfig_Args(t *testing.T) { - oldF := seccomp.GetOutput() - seccomp.SetOutput(t.Log) - t.Cleanup(func() { seccomp.SetOutput(oldF) }) - - testCases := []struct { - name string - conf *bwrap.Config - want []string - }{ - { - "bind", (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), - []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", - }, - }, - { - "dir remount-ro proc dev mqueue", (new(bwrap.Config)). - Dir("/.fortify"). - RemountRO("/home"). - Procfs("/proc"). - DevTmpfs("/dev"). - Mqueue("/dev/mqueue"), - []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", - }, - }, - { - "tmpfs", (new(bwrap.Config)). - Tmpfs("/run/user", 8192). - Tmpfs("/run/dbus", 8192, 0755), - []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", - }, - }, - { - "symlink", (new(bwrap.Config)). - Symlink("/.fortify/sbin/init", "/sbin/init"). - Symlink("/.fortify/sbin/init", "/sbin/init", 0755), - []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", - }, - }, - { - "overlayfs", (new(bwrap.Config)). - Overlay("/etc", "/etc"). - 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"), - []string{ - "--unshare-all", "--unshare-user", - "--disable-userns", "--assert-userns-disabled", - // Overlay("/etc", "/etc") - "--overlay-src", "/etc", "--tmp-overlay", "/etc", - // Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin") - "--overlay-src", "/bin", "--overlay-src", "/usr/bin", - "--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin", - // Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix") - "--overlay-src", "/data/app/org.chromium.Chromium/nix", - "--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix", - }, - }, - { - "copy", (new(bwrap.Config)). - WriteFile("/.fortify/version", make([]byte, 8)). - CopyBind("/etc/group", make([]byte, 8)). - CopyBind("/etc/passwd", make([]byte, 8), true), - []string{ - "--unshare-all", "--unshare-user", - "--disable-userns", "--assert-userns-disabled", - // Write("/.fortify/version", make([]byte, 8)) - "--file", "3", "/.fortify/version", - // CopyBind("/etc/group", make([]byte, 8)) - "--ro-bind-data", "4", "/etc/group", - // CopyBind("/etc/passwd", make([]byte, 8), true) - "--bind-data", "5", "/etc/passwd", - }, - }, - { - "unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{ - User: false, - IPC: false, - PID: false, - 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", - "--disable-userns", "--assert-userns-disabled", - // SetUID(1971) - "--uid", "1971", - // SetGID(100) - "--gid", "100", - }, - }, - { - "hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{ - Hostname: "fortify", - Chdir: "/.fortify", - SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}, - UnsetEnv: []string{"HOME", "HOST"}, - LockFile: []string{"/.fortify/lock"}, - Syscall: new(bwrap.SyscallPolicy), - Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}, - }, - []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", - // Syscall: new(bwrap.SyscallPolicy), - "--seccomp", "3", - // Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755} - "--chmod", "755", "/.fortify/sbin/init", - }, - }, - - { - "xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}). - Symlink("usr/bin", "/bin"). - Symlink("var/home", "/home"). - Symlink("usr/lib", "/lib"). - Symlink("usr/lib64", "/lib64"). - Symlink("run/media", "/media"). - Symlink("var/mnt", "/mnt"). - Symlink("var/opt", "/opt"). - Symlink("sysroot/ostree", "/ostree"). - Symlink("var/roothome", "/root"). - Symlink("usr/sbin", "/sbin"). - Symlink("var/srv", "/srv"). - Bind("/run", "/run", false, true). - Bind("/tmp", "/tmp", false, true). - Bind("/var", "/var", false, true). - Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true). - Bind("/boot", "/boot"). - Bind("/dev", "/dev"). - Bind("/proc", "/proc"). - Bind("/sys", "/sys"). - Bind("/sysroot", "/sysroot"). - Bind("/usr", "/usr"). - Bind("/etc", "/etc"), - []string{ - "--unshare-all", "--unshare-user", - "--disable-userns", "--assert-userns-disabled", - "--clearenv", "--die-with-parent", - "--symlink", "usr/bin", "/bin", - "--symlink", "var/home", "/home", - "--symlink", "usr/lib", "/lib", - "--symlink", "usr/lib64", "/lib64", - "--symlink", "run/media", "/media", - "--symlink", "var/mnt", "/mnt", - "--symlink", "var/opt", "/opt", - "--symlink", "sysroot/ostree", "/ostree", - "--symlink", "var/roothome", "/root", - "--symlink", "usr/sbin", "/sbin", - "--symlink", "var/srv", "/srv", - "--bind", "/run", "/run", - "--bind", "/tmp", "/tmp", - "--bind", "/var", "/var", - "--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", - "--ro-bind", "/boot", "/boot", - "--ro-bind", "/dev", "/dev", - "--ro-bind", "/proc", "/proc", - "--ro-bind", "/sys", "/sys", - "--ro-bind", "/sysroot", "/sysroot", - "--ro-bind", "/usr", "/usr", - "--ro-bind", "/etc", "/etc", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) { - t.Errorf("Args() = %#v, want %#v", got, tc.want) - } - }) - } - - // 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", "", "") - }) -} diff --git a/helper/bwrap/seccomp.go b/helper/bwrap/seccomp.go deleted file mode 100644 index 2253203..0000000 --- a/helper/bwrap/seccomp.go +++ /dev/null @@ -1,86 +0,0 @@ -package bwrap - -import ( - "fmt" - "strconv" - - "git.gensokyo.uk/security/fortify/helper/proc" - "git.gensokyo.uk/security/fortify/sandbox/seccomp" -) - -type SyscallPolicy struct { - // disable fortify extensions - Compat bool `json:"compat"` - // deny development syscalls - DenyDevel bool `json:"deny_devel"` - // deny multiarch/emulation syscalls - Multiarch bool `json:"multiarch"` - // allow PER_LINUX32 - Linux32 bool `json:"linux32"` - // allow AF_CAN - Can bool `json:"can"` - // allow AF_BLUETOOTH - Bluetooth bool `json:"bluetooth"` -} - -func (c *Config) seccompArgs() FDBuilder { - // explicitly disable syscall filter - if c.Syscall == nil { - // nil File skips builder - return new(seccompBuilder) - } - - var ( - opts seccomp.SyscallOpts - optd []string - optCond = [...]struct { - v bool - o seccomp.SyscallOpts - d string - }{ - {!c.Syscall.Compat, seccomp.FlagExt, "fortify"}, - {!c.UserNS, seccomp.FlagDenyNS, "denyns"}, - {c.NewSession, seccomp.FlagDenyTTY, "denytty"}, - {c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"}, - {c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"}, - {c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"}, - {c.Syscall.Can, seccomp.FlagCan, "can"}, - {c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"}, - } - ) - scmpPrintln := seccomp.GetOutput() - if scmpPrintln != nil { - optd = make([]string, 1, len(optCond)+1) - optd[0] = "common" - } - for _, opt := range optCond { - if opt.v { - opts |= opt.o - if scmpPrintln != nil { - optd = append(optd, opt.d) - } - } - } - if scmpPrintln != nil { - scmpPrintln(fmt.Sprintf("seccomp flags: %s", optd)) - } - - return &seccompBuilder{seccomp.NewFile(opts)} -} - -type seccompBuilder struct{ proc.File } - -func (s *seccompBuilder) Len() int { - if s == nil || s.File == nil { - return 0 - } - return 2 -} - -func (s *seccompBuilder) Append(args *[]string) { - if s == nil || s.File == nil { - return - } - - *args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd()))) -} diff --git a/helper/bwrap/sequential.go b/helper/bwrap/sequential.go deleted file mode 100644 index 53ab8aa..0000000 --- a/helper/bwrap/sequential.go +++ /dev/null @@ -1,273 +0,0 @@ -package bwrap - -import ( - "encoding/gob" - "fmt" - "io" - "os" - "strconv" - - "git.gensokyo.uk/security/fortify/helper/proc" -) - -func init() { - gob.Register(new(PermConfig[SymlinkConfig])) - gob.Register(new(PermConfig[*TmpfsConfig])) - gob.Register(new(OverlayConfig)) - gob.Register(new(DataConfig)) -} - -type PositionalArg int - -func (p PositionalArg) String() 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 - - SyncFd - Seccomp - - File - BindData - ROBindData -) - -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", - - SyncFd: "--sync-fd", - Seccomp: "--seccomp", - - File: "--file", - BindData: "--bind-data", - ROBindData: "--ro-bind-data", -} - -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.String(), 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.String(), strconv.Itoa(t.Size)) - } - *args = append(*args, Tmpfs.String(), 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.String(), src) - } - - if o.Persist != nil { - if o.Persist[0] != "" && o.Persist[1] != "" { - // --overlay RWSRC WORKDIR - *args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1]) - } else { - // --ro-overlay - *args = append(*args, ROOverlay.String()) - } - } else { - // --tmp-overlay - *args = append(*args, TmpOverlay.String()) - } - - // 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.String(), 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.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) -} diff --git a/helper/bwrap/static.go b/helper/bwrap/static.go deleted file mode 100644 index 7a8535c..0000000 --- a/helper/bwrap/static.go +++ /dev/null @@ -1,249 +0,0 @@ -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]) - } - } -} diff --git a/helper/bwrap/trivial.go b/helper/bwrap/trivial.go deleted file mode 100644 index 5aa8a1e..0000000 --- a/helper/bwrap/trivial.go +++ /dev/null @@ -1,52 +0,0 @@ -package bwrap - -import ( - "context" - "encoding/gob" - "os" - "strconv" - - "git.gensokyo.uk/security/fortify/helper/proc" -) - -func init() { - gob.Register(new(pairF)) - gob.Register(new(stringF)) -} - -type pairF [3]string - -func (p *pairF) Path() string { return p[2] } -func (p *pairF) Len() int { return len(p) } -func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) } - -type stringF [2]string - -func (s stringF) Path() string { return s[1] } -func (s stringF) Len() int { return len(s) /* compiler replaces this with 2 */ } -func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) } - -func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} } - -type fileF struct { - name string - file *os.File - proc.BaseFile -} - -func (f *fileF) ErrCount() int { return 0 } -func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil } - -func (f *fileF) Len() int { - if f.file == nil { - return 0 - } - return 2 -} - -func (f *fileF) Append(args *[]string) { - if f.file == nil { - return - } - *args = append(*args, f.name, strconv.Itoa(int(f.Fd()))) -} diff --git a/helper/bwrap_test.go b/helper/bwrap_test.go deleted file mode 100644 index a7804e3..0000000 --- a/helper/bwrap_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package helper_test - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "os/exec" - "strings" - "testing" - "time" - - "git.gensokyo.uk/security/fortify/helper" - "git.gensokyo.uk/security/fortify/helper/bwrap" -) - -func TestBwrap(t *testing.T) { - sc := &bwrap.Config{ - Net: true, - Hostname: "localhost", - Chdir: "/proc/nonexistent", - Clearenv: true, - NewSession: true, - DieWithParent: true, - AsInit: true, - } - - t.Run("nonexistent bwrap name", func(t *testing.T) { - bubblewrapName := helper.BubblewrapName - helper.BubblewrapName = "/proc/nonexistent" - t.Cleanup(func() { helper.BubblewrapName = bubblewrapName }) - - h := helper.MustNewBwrap( - context.Background(), - "false", - argsWt, false, - argF, nil, - nil, - sc, nil, - ) - - if err := h.Start(); !errors.Is(err, os.ErrNotExist) { - t.Errorf("Start: error = %v, wantErr %v", - err, os.ErrNotExist) - } - }) - - t.Run("valid new helper nil check", func(t *testing.T) { - if got := helper.MustNewBwrap( - context.TODO(), - "false", - argsWt, false, - argF, nil, - nil, - sc, nil, - ); got == nil { - t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil", - sc, argsWt, "false") - return - } - }) - - t.Run("invalid bwrap config new helper panic", func(t *testing.T) { - defer func() { - wantPanic := "argument contains null character" - if r := recover(); r != wantPanic { - t.Errorf("MustNewBwrap: panic = %q, want %q", - r, wantPanic) - } - }() - - helper.MustNewBwrap( - context.TODO(), - "false", - argsWt, false, - argF, nil, - nil, - &bwrap.Config{Hostname: "\x00"}, nil, - ) - }) - - t.Run("start without pipes", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - stdout, stderr := new(strings.Builder), new(strings.Builder) - h := helper.MustNewBwrap( - ctx, os.Args[0], - nil, false, - argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr; hijackBwrap(cmd) }, - nil, - sc, nil, - ) - - if err := h.Start(); err != nil { - t.Errorf("Start: error = %v", - err) - return - } - - if err := h.Wait(); err != nil { - t.Errorf("Wait() err = %v stderr = %s", - err, stderr) - } - }) - - t.Run("implementation compliance", func(t *testing.T) { - testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { - return helper.MustNewBwrap( - ctx, os.Args[0], - argsWt, stat, - argF, func(cmd *exec.Cmd) { setOutput(&cmd.Stdout, &cmd.Stderr); hijackBwrap(cmd) }, - nil, - sc, nil, - ) - }) - }) -} - -func hijackBwrap(cmd *exec.Cmd) { - if cmd.Args[0] != "bwrap" { - panic(fmt.Sprintf("unexpected argv0 %q", cmd.Args[0])) - } - cmd.Err = nil - cmd.Path = os.Args[0] - cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args...) -} diff --git a/helper/stub.go b/helper/stub.go index cfd4a3f..b1eab71 100644 --- a/helper/stub.go +++ b/helper/stub.go @@ -6,11 +6,7 @@ import ( "io" "os" "strconv" - "strings" "syscall" - - "git.gensokyo.uk/security/fortify/helper/bwrap" - "git.gensokyo.uk/security/fortify/helper/proc" ) // InternalHelperStub is an internal function but exported because it is cross-package; @@ -29,11 +25,7 @@ func InternalHelperStub() { sp = v } - if len(os.Args) > 3 && os.Args[3] == "bwrap" { - bwrapStub() - } else { - genericStub(flagRestoreFiles(3, ap, sp)) - } + genericStub(flagRestoreFiles(3, ap, sp)) os.Exit(0) } @@ -112,43 +104,3 @@ func genericStub(argsFile, statFile *os.File) { <-done } } - -func bwrapStub() { - // the bwrap launcher does not launch with a typical sync fd - argsFile, _ := flagRestoreFiles(4, "1", "0") - - // test args pipe behaviour - func() { - got, want := new(strings.Builder), new(strings.Builder) - if _, err := io.Copy(got, argsFile); err != nil { - panic("cannot read bwrap args: " + err.Error()) - } - - // hardcoded bwrap configuration used by test - sc := &bwrap.Config{ - Net: true, - Hostname: "localhost", - Chdir: "/proc/nonexistent", - Clearenv: true, - NewSession: true, - DieWithParent: true, - AsInit: true, - } - - if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))). - WriteTo(want); err != nil { - panic("cannot read want: " + err.Error()) - } - - if got.String() != want.String() { - panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String()) - } - }() - - if err := syscall.Exec( - flag.CommandLine.Args()[0], - flag.CommandLine.Args(), - os.Environ()); err != nil { - panic("cannot start helper stub: " + err.Error()) - } -} diff --git a/package.nix b/package.nix index 19a4500..ef0cea1 100644 --- a/package.nix +++ b/package.nix @@ -4,7 +4,6 @@ buildGoModule, makeBinaryWrapper, xdg-dbus-proxy, - bubblewrap, pkg-config, libffi, libseccomp, @@ -90,7 +89,6 @@ buildGoModule rec { let appPackages = [ glibc - bubblewrap xdg-dbus-proxy ]; in