diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-03-17 02:55:36 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-03-17 02:55:36 +0900 |
| commit | 24618ab9a1524e8b8986a9bf67667288e642fcf1 (patch) | |
| tree | b3f2a71a2c9bedf937d0fec00092ad9133cb3ec9 /internal | |
| parent | 9ce4706a0766880c072cccd2643d66f614a6a16b (diff) | |
sandbox: move out of internal
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/app/init0/main.go | 2 | ||||
| -rw-r--r-- | internal/app/shim/main.go | 2 | ||||
| -rw-r--r-- | internal/app/shim/manager.go | 2 | ||||
| -rw-r--r-- | internal/output.go | 4 | ||||
| -rw-r--r-- | internal/sandbox/const.go | 6 | ||||
| -rw-r--r-- | internal/sandbox/container.go | 232 | ||||
| -rw-r--r-- | internal/sandbox/container_test.go | 182 | ||||
| -rw-r--r-- | internal/sandbox/executable.go | 26 | ||||
| -rw-r--r-- | internal/sandbox/executable_test.go | 17 | ||||
| -rw-r--r-- | internal/sandbox/init.go | 341 | ||||
| -rw-r--r-- | internal/sandbox/mount.go | 95 | ||||
| -rw-r--r-- | internal/sandbox/msg.go | 43 | ||||
| -rw-r--r-- | internal/sandbox/output.go | 19 | ||||
| -rw-r--r-- | internal/sandbox/overflow.go | 37 | ||||
| -rw-r--r-- | internal/sandbox/params.go | 47 | ||||
| -rw-r--r-- | internal/sandbox/path.go | 75 | ||||
| -rw-r--r-- | internal/sandbox/sequential.go | 180 | ||||
| -rw-r--r-- | internal/sandbox/syscall.go | 41 | ||||
| -rw-r--r-- | internal/sys/std.go | 2 |
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, ¶ms, &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(¶ms.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. |
