aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-03-17 02:55:36 +0900
committerOphestra <cat@gensokyo.uk>2025-03-17 02:55:36 +0900
commit24618ab9a1524e8b8986a9bf67667288e642fcf1 (patch)
treeb3f2a71a2c9bedf937d0fec00092ad9133cb3ec9 /internal
parent9ce4706a0766880c072cccd2643d66f614a6a16b (diff)
sandbox: move out of internal
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
-rw-r--r--internal/app/init0/main.go2
-rw-r--r--internal/app/shim/main.go2
-rw-r--r--internal/app/shim/manager.go2
-rw-r--r--internal/output.go4
-rw-r--r--internal/sandbox/const.go6
-rw-r--r--internal/sandbox/container.go232
-rw-r--r--internal/sandbox/container_test.go182
-rw-r--r--internal/sandbox/executable.go26
-rw-r--r--internal/sandbox/executable_test.go17
-rw-r--r--internal/sandbox/init.go341
-rw-r--r--internal/sandbox/mount.go95
-rw-r--r--internal/sandbox/msg.go43
-rw-r--r--internal/sandbox/output.go19
-rw-r--r--internal/sandbox/overflow.go37
-rw-r--r--internal/sandbox/params.go47
-rw-r--r--internal/sandbox/path.go75
-rw-r--r--internal/sandbox/sequential.go180
-rw-r--r--internal/sandbox/syscall.go41
-rw-r--r--internal/sys/std.go2
19 files changed, 6 insertions, 1347 deletions
diff --git a/internal/app/init0/main.go b/internal/app/init0/main.go
index 46ce4490..7b8d27c6 100644
--- a/internal/app/init0/main.go
+++ b/internal/app/init0/main.go
@@ -11,7 +11,7 @@ import (
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox"
)
const (
diff --git a/internal/app/shim/main.go b/internal/app/shim/main.go
index 94501497..5634be4c 100644
--- a/internal/app/shim/main.go
+++ b/internal/app/shim/main.go
@@ -16,7 +16,7 @@ import (
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox"
)
// everything beyond this point runs as unconstrained target user
diff --git a/internal/app/shim/manager.go b/internal/app/shim/manager.go
index 4b5432b8..4ed75e53 100644
--- a/internal/app/shim/manager.go
+++ b/internal/app/shim/manager.go
@@ -13,7 +13,7 @@ import (
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox"
)
// used by the parent process
diff --git a/internal/output.go b/internal/output.go
index c85f1b51..7b1de30a 100644
--- a/internal/output.go
+++ b/internal/output.go
@@ -2,8 +2,8 @@ package internal
import (
"git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
- "git.gensokyo.uk/security/fortify/seccomp"
+ "git.gensokyo.uk/security/fortify/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
diff --git a/internal/sandbox/const.go b/internal/sandbox/const.go
deleted file mode 100644
index afced969..00000000
--- a/internal/sandbox/const.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package sandbox
-
-const (
- PR_SET_NO_NEW_PRIVS = 0x26
- CAP_SYS_ADMIN = 0x15
-)
diff --git a/internal/sandbox/container.go b/internal/sandbox/container.go
deleted file mode 100644
index afbcc4fd..00000000
--- a/internal/sandbox/container.go
+++ /dev/null
@@ -1,232 +0,0 @@
-package sandbox
-
-import (
- "context"
- "encoding/gob"
- "errors"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path"
- "strconv"
- "syscall"
- "time"
-
- "git.gensokyo.uk/security/fortify/seccomp"
-)
-
-type HardeningFlags uintptr
-
-const (
- FSyscallCompat HardeningFlags = 1 << iota
- FAllowDevel
- FAllowUserns
- FAllowTTY
- FAllowNet
-)
-
-func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
- if flags&FSyscallCompat == 0 {
- opts |= seccomp.FlagExt
- }
- if flags&FAllowDevel == 0 {
- opts |= seccomp.FlagDenyDevel
- }
- if flags&FAllowUserns == 0 {
- opts |= seccomp.FlagDenyNS
- }
- if flags&FAllowTTY == 0 {
- opts |= seccomp.FlagDenyTTY
- }
- return opts
-}
-
-type (
- // Container represents a container environment being prepared or run.
- // None of [Container] methods are safe for concurrent use.
- Container struct {
- // Name of initial process in the container.
- name string
- // Cgroup fd, nil to disable.
- Cgroup *int
- // ExtraFiles passed through to initial process in the container,
- // with behaviour identical to its [exec.Cmd] counterpart.
- ExtraFiles []*os.File
-
- InitParams
- // Custom [exec.Cmd] initialisation function.
- CommandContext func(ctx context.Context) (cmd *exec.Cmd)
-
- // param encoder for shim and init
- setup *gob.Encoder
- // cancels cmd
- cancel context.CancelFunc
-
- Stdin io.Reader
- Stdout io.Writer
- Stderr io.Writer
-
- Cancel func() error
- WaitDelay time.Duration
-
- cmd *exec.Cmd
- ctx context.Context
- }
-
- InitParams struct {
- // Working directory in the container.
- Dir string
- // Initial process environment.
- Env []string
- // Absolute path of initial process in the container. Overrides name.
- Path string
- // Initial process argv.
- Args []string
-
- // Mapped Uid in user namespace.
- Uid int
- // Mapped Gid in user namespace.
- Gid int
- // Hostname value in UTS namespace.
- Hostname string
- // Sequential container setup ops.
- *Ops
- // Extra seccomp options.
- Seccomp seccomp.SyscallOpts
-
- Flags HardeningFlags
- }
-
- Ops []Op
- Op interface {
- apply(params *InitParams) error
-
- Is(op Op) bool
- fmt.Stringer
- }
-)
-
-func (p *Container) Start() error {
- if p.cmd != nil {
- return errors.New("sandbox: already started")
- }
- if p.Ops == nil || len(*p.Ops) == 0 {
- return errors.New("sandbox: starting an empty container")
- }
-
- ctx, cancel := context.WithCancel(p.ctx)
- p.cancel = cancel
-
- var cloneFlags uintptr = syscall.CLONE_NEWIPC |
- syscall.CLONE_NEWUTS |
- syscall.CLONE_NEWCGROUP
- if p.Flags&FAllowNet == 0 {
- cloneFlags |= syscall.CLONE_NEWNET
- }
-
- // map to overflow id to work around ownership checks
- if p.Uid < 1 {
- p.Uid = OverflowUid()
- }
- if p.Gid < 1 {
- p.Gid = OverflowGid()
- }
-
- if p.CommandContext != nil {
- p.cmd = p.CommandContext(ctx)
- } else {
- p.cmd = exec.CommandContext(ctx, MustExecutable())
- p.cmd.Args = []string{"init"}
- }
-
- p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
- p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay
- p.cmd.Dir = "/"
- p.cmd.SysProcAttr = &syscall.SysProcAttr{
- Setsid: p.Flags&FAllowTTY == 0,
- Pdeathsig: syscall.SIGKILL,
-
- Cloneflags: cloneFlags |
- syscall.CLONE_NEWUSER |
- syscall.CLONE_NEWPID |
- syscall.CLONE_NEWNS,
-
- // remain privileged for setup
- AmbientCaps: []uintptr{CAP_SYS_ADMIN},
-
- UseCgroupFD: p.Cgroup != nil,
- }
- if p.cmd.SysProcAttr.UseCgroupFD {
- p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
- }
-
- // place setup pipe before user supplied extra files, this is later restored by init
- if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
- return wrapErrSuffix(err,
- "cannot create shim setup pipe:")
- } else {
- p.setup = e
- p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
- }
- p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
-
- msg.Verbose("starting container init")
- if err := p.cmd.Start(); err != nil {
- return msg.WrapErr(err, err.Error())
- }
- return nil
-}
-
-func (p *Container) Serve() error {
- if p.setup == nil {
- panic("invalid serve")
- }
-
- if p.Path != "" && !path.IsAbs(p.Path) {
- return msg.WrapErr(syscall.EINVAL,
- fmt.Sprintf("invalid executable path %q", p.Path))
- }
-
- if p.Path == "" {
- if p.name == "" {
- p.Path = os.Getenv("SHELL")
- if !path.IsAbs(p.Path) {
- return msg.WrapErr(syscall.EBADE,
- "no command specified and $SHELL is invalid")
- }
- p.name = path.Base(p.Path)
- } else if path.IsAbs(p.name) {
- p.Path = p.name
- } else if v, err := exec.LookPath(p.name); err != nil {
- return msg.WrapErr(err, err.Error())
- } else {
- p.Path = v
- }
- }
-
- setup := p.setup
- p.setup = nil
- return setup.Encode(
- &initParams{
- p.InitParams,
- syscall.Getuid(),
- syscall.Getgid(),
- len(p.ExtraFiles),
- msg.IsVerbose(),
- },
- )
-}
-
-func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
-
-func (p *Container) String() string {
- return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
- p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
-}
-
-func New(ctx context.Context, name string, args ...string) *Container {
- return &Container{name: name, ctx: ctx,
- InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
- }
-}
diff --git a/internal/sandbox/container_test.go b/internal/sandbox/container_test.go
deleted file mode 100644
index 55df60b2..00000000
--- a/internal/sandbox/container_test.go
+++ /dev/null
@@ -1,182 +0,0 @@
-package sandbox_test
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "log"
- "os"
- "os/exec"
- "syscall"
- "testing"
- "time"
-
- "git.gensokyo.uk/security/fortify/fst"
- "git.gensokyo.uk/security/fortify/internal"
- "git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
- "git.gensokyo.uk/security/fortify/ldd"
- "git.gensokyo.uk/security/fortify/seccomp"
- check "git.gensokyo.uk/security/fortify/test/sandbox"
-)
-
-func TestContainer(t *testing.T) {
- {
- oldVerbose := fmsg.Load()
- oldOutput := sandbox.GetOutput()
- internal.InstallFmsg(true)
- t.Cleanup(func() { fmsg.Store(oldVerbose) })
- t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
- }
-
- testCases := []struct {
- name string
- flags sandbox.HardeningFlags
- ops *sandbox.Ops
- mnt []*check.Mntent
- host string
- }{
- {"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
- {"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
- new(sandbox.Ops), nil, "test-minimal"},
- {"tmpfs", 0,
- new(sandbox.Ops).
- Tmpfs(fst.Tmp, 0, 0755),
- []*check.Mntent{
- {FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
- }, "test-tmpfs"},
- {"dev", sandbox.FAllowTTY, // go test output is not a tty
- new(sandbox.Ops).
- Dev("/dev"),
- []*check.Mntent{
- {FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
- {FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
- {FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
- }, ""},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- container := sandbox.New(ctx, os.Args[0], "-test.v",
- "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
- container.Uid = 1000
- container.Gid = 100
- container.Hostname = tc.host
- container.CommandContext = commandContext
- container.Flags |= tc.flags
- container.Stdout, container.Stderr = os.Stdout, os.Stderr
- container.Ops = tc.ops
- if container.Args[5] == "" {
- if name, err := os.Hostname(); err != nil {
- t.Fatalf("cannot get hostname: %v", err)
- } else {
- container.Args[5] = name
- }
- }
-
- container.
- Tmpfs("/tmp", 0, 0755).
- Bind(os.Args[0], os.Args[0], 0)
- // in case test has cgo enabled
- var libPaths []string
- if entries, err := ldd.ExecFilter(ctx,
- commandContext,
- func(v []byte) []byte {
- return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
- }, os.Args[0]); err != nil {
- log.Fatalf("ldd: %v", err)
- } else {
- libPaths = ldd.Path(entries)
- }
- for _, name := range libPaths {
- container.Bind(name, name, 0)
- }
-
- mnt := make([]*check.Mntent, 0, 3+len(libPaths))
- mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
- mnt = append(mnt, tc.mnt...)
- mnt = append(mnt,
- &check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
- &check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
- for _, name := range libPaths {
- mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1})
- }
- mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"})
- mntentWant := new(bytes.Buffer)
- if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
- t.Fatalf("cannot serialise mntent: %v", err)
- }
- container.Stdin = mntentWant
-
- // needs /proc to check mntent
- container.Proc("/proc")
-
- if err := container.Start(); err != nil {
- fmsg.PrintBaseError(err, "start:")
- t.Fatalf("cannot start container: %v", err)
- } else if err = container.Serve(); err != nil {
- fmsg.PrintBaseError(err, "serve:")
- t.Errorf("cannot serve setup params: %v", err)
- }
- if err := container.Wait(); err != nil {
- fmsg.PrintBaseError(err, "wait:")
- t.Fatalf("wait: %v", err)
- }
- })
- }
-}
-
-func TestContainerString(t *testing.T) {
- container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
- container.Flags |= sandbox.FAllowDevel
- container.Seccomp |= seccomp.FlagMultiarch
- want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
- if got := container.String(); got != want {
- t.Errorf("String: %s, want %s", got, want)
- }
-}
-
-func TestHelperInit(t *testing.T) {
- if len(os.Args) != 5 || os.Args[4] != "init" {
- return
- }
- sandbox.SetOutput(fmsg.Output{})
- sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
-}
-
-func TestHelperCheckContainer(t *testing.T) {
- if len(os.Args) != 6 || os.Args[4] != "check" {
- return
- }
-
- t.Run("user", func(t *testing.T) {
- if uid := syscall.Getuid(); uid != 1000 {
- t.Errorf("Getuid: %d, want 1000", uid)
- }
- if gid := syscall.Getgid(); gid != 100 {
- t.Errorf("Getgid: %d, want 100", gid)
- }
- })
- t.Run("hostname", func(t *testing.T) {
- if name, err := os.Hostname(); err != nil {
- t.Fatalf("cannot get hostname: %v", err)
- } else if name != os.Args[5] {
- t.Errorf("Hostname: %q, want %q", name, os.Args[5])
- }
- })
- t.Run("seccomp", func(t *testing.T) { check.MustAssertSeccomp() })
- t.Run("mntent", func(t *testing.T) { check.MustAssertMounts("", "/proc/mounts", "/proc/self/fd/0") })
-}
-
-func commandContext(ctx context.Context) *exec.Cmd {
- return exec.CommandContext(ctx, os.Args[0], "-test.v",
- "-test.run=TestHelperInit", "--", "init")
-}
diff --git a/internal/sandbox/executable.go b/internal/sandbox/executable.go
deleted file mode 100644
index 5f73d238..00000000
--- a/internal/sandbox/executable.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package sandbox
-
-import (
- "log"
- "os"
- "sync"
-)
-
-var (
- executable string
- executableOnce sync.Once
-)
-
-func copyExecutable() {
- if name, err := os.Executable(); err != nil {
- msg.BeforeExit()
- log.Fatalf("cannot read executable path: %v", err)
- } else {
- executable = name
- }
-}
-
-func MustExecutable() string {
- executableOnce.Do(copyExecutable)
- return executable
-}
diff --git a/internal/sandbox/executable_test.go b/internal/sandbox/executable_test.go
deleted file mode 100644
index b11908c5..00000000
--- a/internal/sandbox/executable_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package sandbox_test
-
-import (
- "os"
- "testing"
-
- "git.gensokyo.uk/security/fortify/internal/sandbox"
-)
-
-func TestExecutable(t *testing.T) {
- for i := 0; i < 16; i++ {
- if got := sandbox.MustExecutable(); got != os.Args[0] {
- t.Errorf("MustExecutable: %q, want %q",
- got, os.Args[0])
- }
- }
-}
diff --git a/internal/sandbox/init.go b/internal/sandbox/init.go
deleted file mode 100644
index a6fac1a3..00000000
--- a/internal/sandbox/init.go
+++ /dev/null
@@ -1,341 +0,0 @@
-package sandbox
-
-import (
- "errors"
- "fmt"
- "log"
- "os"
- "os/exec"
- "os/signal"
- "path"
- "runtime"
- "strconv"
- "syscall"
- "time"
-
- "git.gensokyo.uk/security/fortify/seccomp"
-)
-
-const (
- // time to wait for linger processes after death of initial process
- residualProcessTimeout = 5 * time.Second
-
- // intermediate tmpfs mount point
- basePath = "/tmp"
-
- // setup params file descriptor
- setupEnv = "FORTIFY_SETUP"
-)
-
-type initParams struct {
- InitParams
-
- HostUid, HostGid int
- // extra files count
- Count int
- // verbosity pass through
- Verbose bool
-}
-
-func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
- runtime.LockOSThread()
- prepare("init")
-
- if os.Getpid() != 1 {
- log.Fatal("this process must run as pid 1")
- }
-
- /*
- receive setup payload
- */
-
- var (
- params initParams
- closeSetup func() error
- setupFile *os.File
- offsetSetup int
- )
- if f, err := Receive(setupEnv, &params, &setupFile); err != nil {
- if errors.Is(err, ErrInvalid) {
- log.Fatal("invalid setup descriptor")
- }
- if errors.Is(err, ErrNotSet) {
- log.Fatal("FORTIFY_SETUP not set")
- }
-
- log.Fatalf("cannot decode init setup payload: %v", err)
- } else {
- if params.Ops == nil {
- log.Fatal("invalid setup parameters")
- }
-
- setVerbose(params.Verbose)
- msg.Verbose("received setup parameters")
- closeSetup = f
- offsetSetup = int(setupFile.Fd() + 1)
- }
-
- // write uid/gid map here so parent does not need to set dumpable
- if err := SetDumpable(SUID_DUMP_USER); err != nil {
- log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
- }
- if err := os.WriteFile("/proc/self/uid_map",
- append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
- 0); err != nil {
- log.Fatalf("%v", err)
- }
- if err := os.WriteFile("/proc/self/setgroups",
- []byte("deny\n"),
- 0); err != nil && !os.IsNotExist(err) {
- log.Fatalf("%v", err)
- }
- if err := os.WriteFile("/proc/self/gid_map",
- append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
- 0); err != nil {
- log.Fatalf("%v", err)
- }
- if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
- log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
- }
-
- if params.Hostname != "" {
- if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
- log.Fatalf("cannot set hostname: %v", err)
- }
- }
-
- /*
- set up mount points from intermediate root
- */
-
- if err := syscall.Mount("", "/", "",
- syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
- ""); err != nil {
- log.Fatalf("cannot make / rslave: %v", err)
- }
-
- if err := syscall.Mount("rootfs", basePath, "tmpfs",
- syscall.MS_NODEV|syscall.MS_NOSUID,
- ""); err != nil {
- log.Fatalf("cannot mount intermediate root: %v", err)
- }
- if err := os.Chdir(basePath); err != nil {
- log.Fatalf("cannot enter base path: %v", err)
- }
-
- if err := os.Mkdir(sysrootDir, 0755); err != nil {
- log.Fatalf("%v", err)
- }
- if err := syscall.Mount(sysrootDir, sysrootDir, "",
- syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
- ""); err != nil {
- log.Fatalf("cannot bind sysroot: %v", err)
- }
-
- if err := os.Mkdir(hostDir, 0755); err != nil {
- log.Fatalf("%v", err)
- }
- if err := syscall.PivotRoot(basePath, hostDir); err != nil {
- log.Fatalf("cannot pivot into intermediate root: %v", err)
- }
- if err := os.Chdir("/"); err != nil {
- log.Fatalf("%v", err)
- }
-
- for i, op := range *params.Ops {
- msg.Verbosef("mounting %s", op)
- if err := op.apply(&params.InitParams); err != nil {
- msg.PrintBaseErr(err,
- fmt.Sprintf("cannot apply op %d:", i))
- msg.BeforeExit()
- os.Exit(1)
- }
- }
-
- /*
- pivot to sysroot
- */
-
- if err := syscall.Mount(hostDir, hostDir, "",
- syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
- ""); err != nil {
- log.Fatalf("cannot make host root rprivate: %v", err)
- }
- if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
- log.Fatalf("cannot unmount host root: %v", err)
- }
-
- {
- var fd int
- if err := IgnoringEINTR(func() (err error) {
- fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
- return
- }); err != nil {
- log.Fatalf("cannot open intermediate root: %v", err)
- }
- if err := os.Chdir(sysrootPath); err != nil {
- log.Fatalf("%v", err)
- }
-
- if err := syscall.PivotRoot(".", "."); err != nil {
- log.Fatalf("cannot pivot into sysroot: %v", err)
- }
- if err := syscall.Fchdir(fd); err != nil {
- log.Fatalf("cannot re-enter intermediate root: %v", err)
- }
- if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
- log.Fatalf("cannot unmount intemediate root: %v", err)
- }
- if err := os.Chdir("/"); err != nil {
- log.Fatalf("%v", err)
- }
-
- if err := syscall.Close(fd); err != nil {
- log.Fatalf("cannot close intermediate root: %v", err)
- }
- }
-
- /*
- load seccomp filter
- */
-
- if _, _, err := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); err != 0 {
- log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", err)
- }
- if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
- log.Fatalf("cannot load syscall filter: %v", err)
- }
-
- /* at this point CAP_SYS_ADMIN can be dropped, however it is kept for now as it does not increase attack surface */
-
- /*
- pass through extra files
- */
-
- extraFiles := make([]*os.File, params.Count)
- for i := range extraFiles {
- extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
- }
-
- /*
- prepare initial process
- */
-
- cmd := exec.Command(params.Path)
- cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
- cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
- cmd.Args = params.Args
- cmd.Env = params.Env
- cmd.ExtraFiles = extraFiles
- cmd.Dir = params.Dir
-
- if err := cmd.Start(); err != nil {
- log.Fatalf("%v", err)
- }
- msg.Suspend()
-
- /*
- close setup pipe
- */
-
- if err := closeSetup(); err != nil {
- log.Println("cannot close setup pipe:", err)
- // not fatal
- }
-
- /*
- perform init duties
- */
-
- sig := make(chan os.Signal, 2)
- signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
-
- type winfo struct {
- wpid int
- wstatus syscall.WaitStatus
- }
- info := make(chan winfo, 1)
- done := make(chan struct{})
-
- go func() {
- var (
- err error
- wpid = -2
- wstatus syscall.WaitStatus
- )
-
- // keep going until no child process is left
- for wpid != -1 {
- if err != nil {
- break
- }
-
- if wpid != -2 {
- info <- winfo{wpid, wstatus}
- }
-
- err = syscall.EINTR
- for errors.Is(err, syscall.EINTR) {
- wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
- }
- }
- if !errors.Is(err, syscall.ECHILD) {
- log.Println("unexpected wait4 response:", err)
- }
-
- close(done)
- }()
-
- // closed after residualProcessTimeout has elapsed after initial process death
- timeout := make(chan struct{})
-
- r := 2
- for {
- select {
- case s := <-sig:
- if msg.Resume() {
- msg.Verbosef("terminating on %s after process start", s.String())
- } else {
- msg.Verbosef("terminating on %s", s.String())
- }
- msg.BeforeExit()
- os.Exit(0)
- case w := <-info:
- if w.wpid == cmd.Process.Pid {
- // initial process exited, output is most likely available again
- msg.Resume()
-
- switch {
- case w.wstatus.Exited():
- r = w.wstatus.ExitStatus()
- case w.wstatus.Signaled():
- r = 128 + int(w.wstatus.Signal())
- default:
- r = 255
- }
-
- go func() {
- time.Sleep(residualProcessTimeout)
- close(timeout)
- }()
- }
- case <-done:
- msg.BeforeExit()
- os.Exit(r)
- case <-timeout:
- log.Println("timeout exceeded waiting for lingering processes")
- msg.BeforeExit()
- os.Exit(r)
- }
- }
-}
-
-// TryArgv0 calls [Init] if the last element of argv0 is "init".
-func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
- if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
- msg = v
- Init(prepare, setVerbose)
- msg.BeforeExit()
- os.Exit(0)
- }
-}
diff --git a/internal/sandbox/mount.go b/internal/sandbox/mount.go
deleted file mode 100644
index e47e7f5d..00000000
--- a/internal/sandbox/mount.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package sandbox
-
-import (
- "errors"
- "fmt"
- "os"
- "strings"
- "syscall"
-)
-
-const (
- BindOptional = 1 << iota
- BindSource
- BindRecursive
- BindWritable
- BindDevices
-)
-
-func bindMount(src, dest string, flags int) error {
- target := toSysroot(dest)
- var source string
-
- if flags&BindSource == 0 {
- // this is what bwrap does, so the behaviour is kept for now,
- // however recursively resolving links might improve user experience
- if rp, err := realpathHost(src); err != nil {
- if os.IsNotExist(err) {
- if flags&BindOptional != 0 {
- return nil
- } else {
- return msg.WrapErr(err,
- fmt.Sprintf("path %q does not exist", src))
- }
- }
- return msg.WrapErr(err, err.Error())
- } else {
- source = toHost(rp)
- }
- } else if flags&BindOptional != 0 {
- return msg.WrapErr(syscall.EINVAL,
- "flag source excludes optional")
- } else {
- source = toHost(src)
- }
-
- if fi, err := os.Stat(source); err != nil {
- return msg.WrapErr(err, err.Error())
- } else if fi.IsDir() {
- if err = os.MkdirAll(target, 0755); err != nil {
- return wrapErrSuffix(err,
- fmt.Sprintf("cannot create directory %q:", dest))
- }
- } else if err = ensureFile(target, 0444); err != nil {
- if errors.Is(err, syscall.EISDIR) {
- return msg.WrapErr(err,
- fmt.Sprintf("path %q is a directory", dest))
- }
- return wrapErrSuffix(err,
- fmt.Sprintf("cannot create %q:", dest))
- }
-
- var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
- if flags&BindRecursive != 0 {
- mf |= syscall.MS_REC
- }
- if flags&BindWritable == 0 {
- mf |= syscall.MS_RDONLY
- }
- if flags&BindDevices == 0 {
- mf |= syscall.MS_NODEV
- }
- if msg.IsVerbose() {
- if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
- msg.Verbosef("resolved %q flags %#x", target, mf)
- } else {
- msg.Verbosef("resolved %q on %q flags %#x", source, target, mf)
- }
- }
- return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
- fmt.Sprintf("cannot bind %q on %q:", src, dest))
-}
-
-func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
- target := toSysroot(name)
- if err := os.MkdirAll(target, perm); err != nil {
- return err
- }
- opt := fmt.Sprintf("mode=%#o", perm)
- if size > 0 {
- opt += fmt.Sprintf(",size=%d", size)
- }
- return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
- syscall.MS_NOSUID|syscall.MS_NODEV, opt),
- fmt.Sprintf("cannot mount tmpfs on %q:", name))
-}
diff --git a/internal/sandbox/msg.go b/internal/sandbox/msg.go
deleted file mode 100644
index 5ee18509..00000000
--- a/internal/sandbox/msg.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package sandbox
-
-import (
- "log"
- "sync/atomic"
-)
-
-type Msg interface {
- IsVerbose() bool
- Verbose(v ...any)
- Verbosef(format string, v ...any)
- WrapErr(err error, a ...any) error
- PrintBaseErr(err error, fallback string)
-
- Suspend()
- Resume() bool
-
- BeforeExit()
-}
-
-type DefaultMsg struct{ inactive atomic.Bool }
-
-func (msg *DefaultMsg) IsVerbose() bool { return true }
-func (msg *DefaultMsg) Verbose(v ...any) {
- if !msg.inactive.Load() {
- log.Println(v...)
- }
-}
-func (msg *DefaultMsg) Verbosef(format string, v ...any) {
- if !msg.inactive.Load() {
- log.Printf(format, v...)
- }
-}
-
-func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
- log.Println(a...)
- return err
-}
-func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
-
-func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
-func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
-func (msg *DefaultMsg) BeforeExit() {}
diff --git a/internal/sandbox/output.go b/internal/sandbox/output.go
deleted file mode 100644
index de4a104a..00000000
--- a/internal/sandbox/output.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package sandbox
-
-var msg Msg = new(DefaultMsg)
-
-func GetOutput() Msg { return msg }
-func SetOutput(v Msg) {
- if v == nil {
- msg = new(DefaultMsg)
- } else {
- msg = v
- }
-}
-
-func wrapErrSuffix(err error, a ...any) error {
- if err == nil {
- return nil
- }
- return msg.WrapErr(err, append(a, err)...)
-}
diff --git a/internal/sandbox/overflow.go b/internal/sandbox/overflow.go
deleted file mode 100644
index ebaad704..00000000
--- a/internal/sandbox/overflow.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package sandbox
-
-import (
- "bytes"
- "log"
- "os"
- "strconv"
- "sync"
-)
-
-var (
- ofUid int
- ofGid int
- ofOnce sync.Once
-)
-
-const (
- ofUidPath = "/proc/sys/kernel/overflowuid"
- ofGidPath = "/proc/sys/kernel/overflowgid"
-)
-
-func mustReadOverflow() {
- if v, err := os.ReadFile(ofUidPath); err != nil {
- log.Fatalf("cannot read %q: %v", ofUidPath, err)
- } else if ofUid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
- log.Fatalf("cannot interpret %q: %v", ofUidPath, err)
- }
-
- if v, err := os.ReadFile(ofGidPath); err != nil {
- log.Fatalf("cannot read %q: %v", ofGidPath, err)
- } else if ofGid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
- log.Fatalf("cannot interpret %q: %v", ofGidPath, err)
- }
-}
-
-func OverflowUid() int { ofOnce.Do(mustReadOverflow); return ofUid }
-func OverflowGid() int { ofOnce.Do(mustReadOverflow); return ofGid }
diff --git a/internal/sandbox/params.go b/internal/sandbox/params.go
deleted file mode 100644
index 5b698747..00000000
--- a/internal/sandbox/params.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package sandbox
-
-import (
- "encoding/gob"
- "errors"
- "os"
- "strconv"
-)
-
-var (
- ErrNotSet = errors.New("environment variable not set")
- ErrInvalid = errors.New("bad file descriptor")
-)
-
-// Setup appends the read end of a pipe for setup params transmission and returns its fd.
-func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
- if r, w, err := os.Pipe(); err != nil {
- return -1, nil, err
- } else {
- fd := 3 + len(*extraFiles)
- *extraFiles = append(*extraFiles, r)
- return fd, gob.NewEncoder(w), nil
- }
-}
-
-// Receive retrieves setup fd from the environment and receives params.
-func Receive(key string, e any, v **os.File) (func() error, error) {
- var setup *os.File
-
- if s, ok := os.LookupEnv(key); !ok {
- return nil, ErrNotSet
- } else {
- if fd, err := strconv.Atoi(s); err != nil {
- return nil, err
- } else {
- setup = os.NewFile(uintptr(fd), "setup")
- if setup == nil {
- return nil, ErrInvalid
- }
- if v != nil {
- *v = setup
- }
- }
- }
-
- return setup.Close, gob.NewDecoder(setup).Decode(e)
-}
diff --git a/internal/sandbox/path.go b/internal/sandbox/path.go
deleted file mode 100644
index 6c5a6e2c..00000000
--- a/internal/sandbox/path.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package sandbox
-
-import (
- "errors"
- "io/fs"
- "os"
- "path"
- "strings"
- "syscall"
-)
-
-const (
- hostPath = "/" + hostDir
- hostDir = "host"
- sysrootPath = "/" + sysrootDir
- sysrootDir = "sysroot"
-)
-
-func toSysroot(name string) string {
- name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
- return path.Join(sysrootPath, name)
-}
-
-func toHost(name string) string {
- name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
- return path.Join(hostPath, name)
-}
-
-func realpathHost(name string) (string, error) {
- source := toHost(name)
- rp, err := os.Readlink(source)
-
- if err != nil {
- if errors.Is(err, syscall.EINVAL) {
- // not a symlink
- return name, nil
- }
- return "", err
- }
-
- if !path.IsAbs(rp) {
- return name, nil
- }
- msg.Verbosef("path %q resolves to %q", name, rp)
- return rp, nil
-}
-
-func createFile(name string, perm os.FileMode, content []byte) error {
- if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
- return err
- }
- f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
- if err != nil {
- return err
- }
- if content != nil {
- _, err = f.Write(content)
- }
- return errors.Join(f.Close(), err)
-}
-
-func ensureFile(name string, perm os.FileMode) error {
- fi, err := os.Stat(name)
- if err != nil {
- if !os.IsNotExist(err) {
- return err
- }
- return createFile(name, perm, nil)
- }
-
- if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
- err = syscall.EISDIR
- }
- return err
-}
diff --git a/internal/sandbox/sequential.go b/internal/sandbox/sequential.go
deleted file mode 100644
index aa4bb18e..00000000
--- a/internal/sandbox/sequential.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package sandbox
-
-import (
- "encoding/gob"
- "fmt"
- "math"
- "os"
- "path"
- "syscall"
- "unsafe"
-)
-
-func init() { gob.Register(new(BindMount)) }
-
-// BindMount bind mounts host path Source on container path Target.
-type BindMount struct {
- Source, Target string
-
- Flags int
-}
-
-func (b *BindMount) apply(*InitParams) error {
- if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
- return msg.WrapErr(syscall.EBADE,
- "path is not absolute")
- }
- return bindMount(b.Source, b.Target, b.Flags)
-}
-
-func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
-func (b *BindMount) String() string {
- if b.Source == b.Target {
- return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
- }
- return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
-}
-func (f *Ops) Bind(source, target string, flags int) *Ops {
- *f = append(*f, &BindMount{source, target, flags | BindRecursive})
- return f
-}
-
-func init() { gob.Register(new(MountProc)) }
-
-// MountProc mounts a private proc instance on container Path.
-type MountProc struct {
- Path string
-}
-
-func (p *MountProc) apply(*InitParams) error {
- if !path.IsAbs(p.Path) {
- return msg.WrapErr(syscall.EBADE,
- fmt.Sprintf("path %q is not absolute", p.Path))
- }
-
- target := toSysroot(p.Path)
- if err := os.MkdirAll(target, 0755); err != nil {
- return msg.WrapErr(err, err.Error())
- }
- return wrapErrSuffix(syscall.Mount("proc", target, "proc",
- syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
- fmt.Sprintf("cannot mount proc on %q:", p.Path))
-}
-
-func init() { gob.Register(new(MountDev)) }
-
-// MountDev mounts dev on container Path.
-type MountDev struct {
- Path string
-}
-
-func (d *MountDev) apply(params *InitParams) error {
- if !path.IsAbs(d.Path) {
- return msg.WrapErr(syscall.EBADE,
- fmt.Sprintf("path %q is not absolute", d.Path))
- }
- target := toSysroot(d.Path)
-
- if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil {
- return err
- }
-
- for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
- if err := bindMount(
- "/dev/"+name, path.Join(d.Path, name),
- BindSource|BindDevices,
- ); err != nil {
- return err
- }
- }
- for i, name := range []string{"stdin", "stdout", "stderr"} {
- if err := os.Symlink(
- "/proc/self/fd/"+string(rune(i+'0')),
- path.Join(target, name),
- ); err != nil {
- return msg.WrapErr(err, err.Error())
- }
- }
- for _, pair := range [][2]string{
- {"/proc/self/fd", "fd"},
- {"/proc/kcore", "core"},
- {"pts/ptmx", "ptmx"},
- } {
- if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
- return msg.WrapErr(err, err.Error())
- }
- }
-
- devPtsPath := path.Join(target, "pts")
- for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
- if err := os.Mkdir(name, 0755); err != nil {
- return msg.WrapErr(err, err.Error())
- }
- }
-
- if err := syscall.Mount("devpts", devPtsPath, "devpts",
- syscall.MS_NOSUID|syscall.MS_NOEXEC,
- "newinstance,ptmxmode=0666,mode=620"); err != nil {
- return wrapErrSuffix(err,
- fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
- }
-
- if params.Flags&FAllowTTY != 0 {
- var buf [8]byte
- if _, _, errno := syscall.Syscall(
- syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
- uintptr(unsafe.Pointer(&buf[0])),
- ); errno == 0 {
- if err := bindMount(
- "/proc/self/fd/1", path.Join(d.Path, "console"),
- BindDevices,
- ); err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-func (d *MountDev) Is(op Op) bool { vd, ok := op.(*MountDev); return ok && *d == *vd }
-func (d *MountDev) String() string { return fmt.Sprintf("dev on %q", d.Path) }
-func (f *Ops) Dev(dest string) *Ops {
- *f = append(*f, &MountDev{dest})
- return f
-}
-
-func (p *MountProc) Is(op Op) bool { vp, ok := op.(*MountProc); return ok && *p == *vp }
-func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
-func (f *Ops) Proc(dest string) *Ops {
- *f = append(*f, &MountProc{dest})
- return f
-}
-
-func init() { gob.Register(new(MountTmpfs)) }
-
-// MountTmpfs mounts tmpfs on container Path.
-type MountTmpfs struct {
- Path string
- Size int
- Perm os.FileMode
-}
-
-func (t *MountTmpfs) apply(*InitParams) error {
- if !path.IsAbs(t.Path) {
- return msg.WrapErr(syscall.EBADE,
- fmt.Sprintf("path %q is not absolute", t.Path))
- }
- if t.Size < 0 || t.Size > math.MaxUint>>1 {
- return msg.WrapErr(syscall.EBADE,
- fmt.Sprintf("size %d out of bounds", t.Size))
- }
- return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
-}
-
-func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
-func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
-func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
- *f = append(*f, &MountTmpfs{dest, size, perm})
- return f
-}
diff --git a/internal/sandbox/syscall.go b/internal/sandbox/syscall.go
deleted file mode 100644
index a17928f7..00000000
--- a/internal/sandbox/syscall.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package sandbox
-
-import "syscall"
-
-const (
- SUID_DUMP_DISABLE = iota
- SUID_DUMP_USER
-)
-
-func SetDumpable(dumpable uintptr) error {
- // linux/sched/coredump.h
- if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
- return errno
- }
-
- return nil
-}
-
-func SetPdeathsig(sig syscall.Signal) error {
- if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 {
- return errno
- }
-
- return nil
-}
-
-// IgnoringEINTR makes a function call and repeats it if it returns an
-// EINTR error. This appears to be required even though we install all
-// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
-// Also #20400 and #36644 are issues in which a signal handler is
-// installed without setting SA_RESTART. None of these are the common case,
-// but there are enough of them that it seems that we can't avoid
-// an EINTR loop.
-func IgnoringEINTR(fn func() error) error {
- for {
- err := fn()
- if err != syscall.EINTR {
- return err
- }
- }
-}
diff --git a/internal/sys/std.go b/internal/sys/std.go
index 0145eb23..132f4c13 100644
--- a/internal/sys/std.go
+++ b/internal/sys/std.go
@@ -15,7 +15,7 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
- "git.gensokyo.uk/security/fortify/internal/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox"
)
// Std implements System using the standard library.