From ba76e2919b6791c78d2c1e79cb50dbc4731a5b00 Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Mon, 30 Sep 2024 00:23:57 +0900 Subject: [PATCH] bwrap: implement argument builder Signed-off-by: Ophestra Umiker --- bwrap/config.bool.go | 64 +++++++++ bwrap/config.go | 265 ++++++++++++++++++++++++++++++++++++++ bwrap/config.int.go | 22 ++++ bwrap/config.interface.go | 34 +++++ bwrap/config.pair.go | 54 ++++++++ bwrap/config.string.go | 35 +++++ bwrap/config_test.go | 88 +++++++++++++ 7 files changed, 562 insertions(+) create mode 100644 bwrap/config.bool.go create mode 100644 bwrap/config.go create mode 100644 bwrap/config.int.go create mode 100644 bwrap/config.interface.go create mode 100644 bwrap/config.pair.go create mode 100644 bwrap/config.string.go create mode 100644 bwrap/config_test.go diff --git a/bwrap/config.bool.go b/bwrap/config.bool.go new file mode 100644 index 0000000..81e6ab7 --- /dev/null +++ b/bwrap/config.bool.go @@ -0,0 +1,64 @@ +package bwrap + +const ( + UnshareAll = iota + UnshareUser + UnshareIPC + UnsharePID + UnshareNet + UnshareUTS + UnshareCGroup + ShareNet + + UserNS + Clearenv + + NewSession + DieWithParent + AsInit + + boolC +) + +var boolArgs = func() (b [boolC][]string) { + b[UnshareAll] = []string{"--unshare-all"} + b[UnshareUser] = []string{"--unshare-user"} + b[UnshareIPC] = []string{"--unshare-ipc"} + b[UnsharePID] = []string{"--unshare-pid"} + b[UnshareNet] = []string{"--unshare-net"} + b[UnshareUTS] = []string{"--unshare-uts"} + b[UnshareCGroup] = []string{"--unshare-cgroup"} + b[ShareNet] = []string{"--share-net"} + + b[UserNS] = []string{"--disable-userns", "--assert-userns-disabled"} + b[Clearenv] = []string{"--clearenv"} + + b[NewSession] = []string{"--new-session"} + b[DieWithParent] = []string{"--die-with-parent"} + b[AsInit] = []string{"--as-pid-1"} + + return +}() + +func (c *Config) boolArgs() (b [boolC]bool) { + 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 + } + + b[UserNS] = !c.UserNS + b[Clearenv] = c.Clearenv + + b[NewSession] = c.NewSession + b[DieWithParent] = c.DieWithParent + b[AsInit] = c.AsInit + + return +} diff --git a/bwrap/config.go b/bwrap/config.go new file mode 100644 index 0000000..c20a33b --- /dev/null +++ b/bwrap/config.go @@ -0,0 +1,265 @@ +package bwrap + +import ( + "os" + "strconv" +) + +func (c *Config) Args() (args []string) { + b := c.boolArgs() + n := c.intArgs() + s := c.stringArgs() + p := c.pairArgs() + g := c.interfaceArgs() + + argc := 0 + for i, arg := range b { + if arg { + argc += len(boolArgs[i]) + } + } + for _, arg := range n { + if arg != nil { + argc += 2 + } + } + for _, arg := range s { + argc += len(arg) * 2 + } + for _, arg := range p { + argc += len(arg) * 3 + } + + args = make([]string, 0, argc) + for i, arg := range b { + if arg { + args = append(args, boolArgs[i]...) + } + } + for i, arg := range n { + if arg != nil { + args = append(args, intArgs[i], strconv.Itoa(*arg)) + } + } + for i, arg := range s { + for _, v := range arg { + args = append(args, stringArgs[i], v) + } + } + for i, arg := range p { + for _, v := range arg { + args = append(args, pairArgs[i], v[0], v[1]) + } + } + for i, arg := range g { + for _, v := range arg { + args = append(args, v.Value(interfaceArgs[i])...) + } + } + + return +} + +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"` + + // bind mount host path on sandbox + // (--bind SRC DEST) + Bind [][2]string `json:"bind,omitempty"` + // equal to Bind but ignores non-existent host path + // (--bind-try SRC DEST) + BindTry [][2]string `json:"bind_try,omitempty"` + + // bind mount host path on sandbox, allowing device access + // (--dev-bind SRC DEST) + DevBind [][2]string `json:"dev_bind,omitempty"` + // equal to DevBind but ignores non-existent host path + // (--dev-bind-try SRC DEST) + DevBindTry [][2]string `json:"dev_bind_try,omitempty"` + + // bind mount host path readonly on sandbox + // (--ro-bind SRC DEST) + ROBind [][2]string `json:"ro_bind,omitempty"` + // equal to ROBind but ignores non-existent host path + // (--ro-bind-try SRC DEST) + ROBindTry [][2]string `json:"ro_bind_try,omitempty"` + + // remount path as readonly; does not recursively remount + // (--remount-ro DEST) + RemountRO []string `json:"remount_ro,omitempty"` + + // mount new procfs in sandbox + // (--proc DEST) + Procfs []PermConfig[string] `json:"proc,omitempty"` + // mount new dev in sandbox + // (--dev DEST) + DevTmpfs []PermConfig[string] `json:"dev,omitempty"` + // mount new tmpfs in sandbox + // (--tmpfs DEST) + Tmpfs []PermConfig[TmpfsConfig] `json:"tmpfs,omitempty"` + // mount new mqueue in sandbox + // (--mqueue DEST) + Mqueue []PermConfig[string] `json:"mqueue,omitempty"` + // create dir in sandbox + // (--dir DEST) + Dir []PermConfig[string] `json:"dir,omitempty"` + // create symlink within sandbox + // (--symlink SRC DEST) + Symlink []PermConfig[[2]string] `json:"symlink,omitempty"` + + // change permissions (must already exist) + // (--chmod OCTAL PATH) + Chmod map[string]os.FileMode `json:"chmod,omitempty"` + + // 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) + --sync-fd FD Keep this fd open while sandbox is running + --exec-label LABEL Exec label for the sandbox + --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 + --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 + --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"` +} + +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"` +} + +type argOf interface { + Value(arg string) (args []string) +} + +func copyToArgOfSlice[T [2]string | string | TmpfsConfig](src []PermConfig[T]) (dst []argOf) { + dst = make([]argOf, len(src)) + for i, arg := range src { + dst[i] = arg + } + return +} + +type PermConfig[T [2]string | string | TmpfsConfig] struct { + // set permissions of next argument + // (--perms OCTAL) + Mode *os.FileMode `json:"mode,omitempty"` + // path to get the new permission + // (--bind-data, --file, etc.) + Path T +} + +func (p PermConfig[T]) Value(arg string) (args []string) { + // max possible size + if p.Mode != nil { + args = make([]string, 0, 6) + args = append(args, "--perms", strconv.Itoa(int(*p.Mode))) + } else { + args = make([]string, 0, 4) + } + + switch v := any(p.Path).(type) { + case string: + args = append(args, arg, v) + return + case [2]string: + args = append(args, arg, v[0], v[1]) + return + case TmpfsConfig: + if arg != "--tmpfs" { + panic("unreachable") + } + + if v.Size > 0 { + args = append(args, "--size", strconv.Itoa(v.Size)) + } + args = append(args, arg, v.Dir) + return + default: + panic("unreachable") + } +} diff --git a/bwrap/config.int.go b/bwrap/config.int.go new file mode 100644 index 0000000..27c88c3 --- /dev/null +++ b/bwrap/config.int.go @@ -0,0 +1,22 @@ +package bwrap + +const ( + UID = iota + GID + + intC +) + +var intArgs = func() (n [intC]string) { + n[UID] = "--uid" + n[GID] = "--gid" + + return +}() + +func (c *Config) intArgs() (n [intC]*int) { + n[UID] = c.UID + n[GID] = c.GID + + return +} diff --git a/bwrap/config.interface.go b/bwrap/config.interface.go new file mode 100644 index 0000000..eb559c1 --- /dev/null +++ b/bwrap/config.interface.go @@ -0,0 +1,34 @@ +package bwrap + +const ( + Procfs = iota + DevTmpfs + Tmpfs + Mqueue + Dir + Symlink + + interfaceC +) + +var interfaceArgs = func() (g [interfaceC]string) { + g[Procfs] = "--proc" + g[DevTmpfs] = "--dev" + g[Tmpfs] = "--tmpfs" + g[Mqueue] = "--mqueue" + g[Dir] = "--dir" + g[Symlink] = "--symlink" + + return +}() + +func (c *Config) interfaceArgs() (g [interfaceC][]argOf) { + g[Procfs] = copyToArgOfSlice(c.Procfs) + g[DevTmpfs] = copyToArgOfSlice(c.DevTmpfs) + g[Tmpfs] = copyToArgOfSlice(c.Tmpfs) + g[Mqueue] = copyToArgOfSlice(c.Mqueue) + g[Dir] = copyToArgOfSlice(c.Dir) + g[Symlink] = copyToArgOfSlice(c.Symlink) + + return +} diff --git a/bwrap/config.pair.go b/bwrap/config.pair.go new file mode 100644 index 0000000..b0b1001 --- /dev/null +++ b/bwrap/config.pair.go @@ -0,0 +1,54 @@ +package bwrap + +import "strconv" + +const ( + SetEnv = iota + + Bind + BindTry + DevBind + DevBindTry + ROBind + ROBindTry + + Chmod + + pairC +) + +var pairArgs = func() (n [pairC]string) { + n[SetEnv] = "--setenv" + + n[Bind] = "--bind" + n[BindTry] = "--bind-try" + n[DevBind] = "--dev-bind" + n[DevBindTry] = "--dev-bind-try" + n[ROBind] = "--ro-bind" + n[ROBindTry] = "--ro-bind-try" + + n[Chmod] = "--chmod" + + return +}() + +func (c *Config) pairArgs() (n [pairC][][2]string) { + n[SetEnv] = make([][2]string, 0, len(c.SetEnv)) + for k, v := range c.SetEnv { + n[SetEnv] = append(n[SetEnv], [2]string{k, v}) + } + + n[Bind] = c.Bind + n[BindTry] = c.BindTry + n[DevBind] = c.DevBind + n[DevBindTry] = c.DevBindTry + n[ROBind] = c.ROBind + n[ROBindTry] = c.ROBindTry + + n[Chmod] = make([][2]string, 0, len(c.Chmod)) + for path, octal := range c.Chmod { + n[Chmod] = append(n[Chmod], [2]string{strconv.Itoa(int(octal)), path}) + } + + return +} diff --git a/bwrap/config.string.go b/bwrap/config.string.go new file mode 100644 index 0000000..c5c7a41 --- /dev/null +++ b/bwrap/config.string.go @@ -0,0 +1,35 @@ +package bwrap + +const ( + Hostname = iota + Chdir + UnsetEnv + LockFile + RemountRO + + stringC +) + +var stringArgs = func() (n [stringC]string) { + n[Hostname] = "--hostname" + n[Chdir] = "--chdir" + n[UnsetEnv] = "--unsetenv" + n[LockFile] = "--lock-file" + n[RemountRO] = "--remount-ro" + + return +}() + +func (c *Config) stringArgs() (n [stringC][]string) { + if c.Hostname != "" { + n[Hostname] = []string{c.Hostname} + } + if c.Chdir != "" { + n[Chdir] = []string{c.Chdir} + } + n[UnsetEnv] = c.UnsetEnv + n[LockFile] = c.LockFile + n[RemountRO] = c.RemountRO + + return +} diff --git a/bwrap/config_test.go b/bwrap/config_test.go new file mode 100644 index 0000000..d43c671 --- /dev/null +++ b/bwrap/config_test.go @@ -0,0 +1,88 @@ +package bwrap + +import ( + "slices" + "testing" +) + +func TestConfig_Args(t *testing.T) { + testCases := []struct { + name string + conf *Config + want []string + }{ + { + name: "xdg-dbus-proxy constraint sample", + conf: &Config{ + Unshare: nil, + UserNS: false, + Clearenv: true, + Symlink: []PermConfig[[2]string]{ + {Path: [2]string{"usr/bin", "/bin"}}, + {Path: [2]string{"var/home", "/home"}}, + {Path: [2]string{"usr/lib", "/lib"}}, + {Path: [2]string{"usr/lib64", "/lib64"}}, + {Path: [2]string{"run/media", "/media"}}, + {Path: [2]string{"var/mnt", "/mnt"}}, + {Path: [2]string{"var/opt", "/opt"}}, + {Path: [2]string{"sysroot/ostree", "/ostree"}}, + {Path: [2]string{"var/roothome", "/root"}}, + {Path: [2]string{"usr/sbin", "/sbin"}}, + {Path: [2]string{"var/srv", "/srv"}}, + }, + Bind: [][2]string{ + {"/run", "/run"}, + {"/tmp", "/tmp"}, + {"/var", "/var"}, + {"/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/"}, + }, + ROBind: [][2]string{ + {"/boot", "/boot"}, + {"/dev", "/dev"}, + {"/proc", "/proc"}, + {"/sys", "/sys"}, + {"/sysroot", "/sysroot"}, + {"/usr", "/usr"}, + {"/etc", "/etc"}, + }, + DieWithParent: true, + }, + want: []string{ + "--unshare-all", + "--disable-userns", + "--assert-userns-disabled", + "--clearenv", + "--die-with-parent", + "--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", + "--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"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.conf.Args(); !slices.Equal(got, tc.want) { + t.Errorf("Args() = %#v, want %#v", got, tc.want) + } + }) + } +}