sandbox: native container tooling
This should eventually replace bwrap. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
beb3918809
commit
9b1a60b5c9
@ -140,6 +140,7 @@
|
||||
gcc
|
||||
pkg-config
|
||||
wayland-scanner
|
||||
bubblewrap
|
||||
]
|
||||
++ (
|
||||
with pkgs.pkgsStatic;
|
||||
|
6
internal/sandbox/const.go
Normal file
6
internal/sandbox/const.go
Normal file
@ -0,0 +1,6 @@
|
||||
package sandbox
|
||||
|
||||
const (
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
CAP_SYS_ADMIN = 0x15
|
||||
)
|
212
internal/sandbox/container.go
Normal file
212
internal/sandbox/container.go
Normal file
@ -0,0 +1,212 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
type HardeningFlags uintptr
|
||||
|
||||
const (
|
||||
FAllowUserns HardeningFlags = 1 << iota
|
||||
FAllowTTY
|
||||
FAllowNet
|
||||
)
|
||||
|
||||
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
|
||||
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)
|
||||
// mapped uid in user namespace
|
||||
Uid int
|
||||
// mapped gid in user namespace
|
||||
Gid int
|
||||
|
||||
// param encoder for shim and init
|
||||
setup *gob.Encoder
|
||||
// cancels cmd
|
||||
cancel context.CancelFunc
|
||||
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
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
|
||||
|
||||
// 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() error
|
||||
|
||||
Is(op Op) bool
|
||||
fmt.Stringer
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Container) Start() error {
|
||||
if p.cmd != nil {
|
||||
panic("attempted to start twice")
|
||||
}
|
||||
|
||||
c, 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()
|
||||
}
|
||||
|
||||
p.cmd = p.CommandContext(c)
|
||||
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||
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,
|
||||
|
||||
UidMappings: []syscall.SysProcIDMap{{p.Uid, syscall.Getuid(), 1}},
|
||||
GidMappings: []syscall.SysProcIDMap{{p.Gid, syscall.Getgid(), 1}},
|
||||
// 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 := proc.Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
return fmsg.WrapErrorSuffix(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...)
|
||||
|
||||
fmsg.Verbose("starting container init")
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return fmsg.WrapError(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 fmsg.WrapError(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 fmsg.WrapError(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 fmsg.WrapError(err, err.Error())
|
||||
} else {
|
||||
p.Path = v
|
||||
}
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
return setup.Encode(
|
||||
&initParams{
|
||||
p.InitParams,
|
||||
len(p.ExtraFiles),
|
||||
fmsg.Load(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
|
||||
|
||||
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)},
|
||||
CommandContext: func(ctx context.Context) (cmd *exec.Cmd) {
|
||||
cmd = exec.CommandContext(ctx, internal.MustExecutable())
|
||||
cmd.Args = []string{"init"}
|
||||
return
|
||||
},
|
||||
}
|
||||
}
|
149
internal/sandbox/container_test.go
Normal file
149
internal/sandbox/container_test.go
Normal file
@ -0,0 +1,149 @@
|
||||
package sandbox_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"slices"
|
||||
"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"
|
||||
check "git.gensokyo.uk/security/fortify/test/sandbox"
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
{
|
||||
oldVerbose := fmsg.Load()
|
||||
fmsg.Store(true)
|
||||
t.Cleanup(func() { fmsg.Store(oldVerbose) })
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ops *sandbox.Ops
|
||||
mnt []*check.Mntent
|
||||
host string
|
||||
}{
|
||||
{"minimal", new(sandbox.Ops), nil, "test-minimal"},
|
||||
{"tmpfs",
|
||||
new(sandbox.Ops).
|
||||
Tmpfs(fst.Tmp, 0, 0755),
|
||||
[]*check.Mntent{
|
||||
{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
|
||||
}, "test-tmpfs"},
|
||||
}
|
||||
|
||||
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.Hostname = tc.host
|
||||
container.CommandContext = func(ctx context.Context) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||
"-test.run=TestHelperInit", "--", "init")
|
||||
}
|
||||
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.Exec(ctx, os.Args[0]); err != nil {
|
||||
log.Fatalf("ldd: %v", err)
|
||||
} else {
|
||||
libPathsM := make(map[string]struct{}, len(entries))
|
||||
for _, ent := range entries {
|
||||
if path.IsAbs(ent.Path) {
|
||||
libPathsM[path.Dir(ent.Path)] = struct{}{}
|
||||
}
|
||||
if path.IsAbs(ent.Name) {
|
||||
libPathsM[path.Dir(ent.Name)] = struct{}{}
|
||||
}
|
||||
}
|
||||
libPaths = make([]string, 0, len(libPathsM))
|
||||
for name := range libPathsM {
|
||||
libPaths = append(libPaths, name)
|
||||
}
|
||||
slices.Sort(libPaths)
|
||||
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 TestHelperInit(t *testing.T) {
|
||||
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||
return
|
||||
}
|
||||
sandbox.Init(internal.Exit)
|
||||
}
|
||||
|
||||
func TestHelperCheckContainer(t *testing.T) {
|
||||
if len(os.Args) != 6 || os.Args[4] != "check" {
|
||||
return
|
||||
}
|
||||
|
||||
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") })
|
||||
}
|
317
internal/sandbox/init.go
Normal file
317
internal/sandbox/init.go
Normal file
@ -0,0 +1,317 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
// extra files count
|
||||
Count int
|
||||
// verbosity pass through
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func Init(exit func(code int)) {
|
||||
runtime.LockOSThread()
|
||||
fmsg.Prepare("init")
|
||||
|
||||
if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
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 := proc.Receive(setupEnv, ¶ms, &setupFile); err != nil {
|
||||
if errors.Is(err, proc.ErrInvalid) {
|
||||
log.Fatal("invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, proc.ErrNotSet) {
|
||||
log.Fatal("FORTIFY_SETUP not set")
|
||||
}
|
||||
|
||||
log.Fatalf("cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
fmsg.Store(params.Verbose)
|
||||
fmsg.Verbose("received setup parameters")
|
||||
if params.Verbose {
|
||||
seccomp.CPrintln = fmsg.Verbose
|
||||
}
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFile.Fd() + 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
fmsg.Verbosef("mounting %s", op)
|
||||
if err := op.apply(); err != nil {
|
||||
fmsg.PrintBaseError(err,
|
||||
fmt.Sprintf("cannot apply op %d:", i))
|
||||
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 := internal.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)
|
||||
}
|
||||
fmsg.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 fmsg.Resume() {
|
||||
fmsg.Verbosef("terminating on %s after process start", s.String())
|
||||
} else {
|
||||
fmsg.Verbosef("terminating on %s", s.String())
|
||||
}
|
||||
exit(0)
|
||||
case w := <-info:
|
||||
if w.wpid == cmd.Process.Pid {
|
||||
// initial process exited, output is most likely available again
|
||||
fmsg.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:
|
||||
exit(r)
|
||||
case <-timeout:
|
||||
log.Println("timeout exceeded waiting for lingering processes")
|
||||
exit(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TryArgv0 calls [Init] if the last element of argv0 is "init".
|
||||
func TryArgv0() {
|
||||
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
|
||||
Init(internal.Exit)
|
||||
internal.Exit(0)
|
||||
}
|
||||
}
|
77
internal/sandbox/path.go
Normal file
77
internal/sandbox/path.go
Normal file
@ -0,0 +1,77 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
fmsg.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
|
||||
}
|
169
internal/sandbox/sequential.go
Normal file
169
internal/sandbox/sequential.go
Normal file
@ -0,0 +1,169 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(BindMount)) }
|
||||
|
||||
const (
|
||||
BindOptional = 1 << iota
|
||||
BindRecursive
|
||||
BindWritable
|
||||
BindDevices
|
||||
)
|
||||
|
||||
// BindMount bind mounts host path Source on container path Target.
|
||||
type BindMount struct {
|
||||
Source, Target string
|
||||
|
||||
Flags int
|
||||
}
|
||||
|
||||
func (b *BindMount) apply() error {
|
||||
if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
|
||||
return syscall.EBADE
|
||||
}
|
||||
target := toSysroot(b.Target)
|
||||
var source string
|
||||
|
||||
// this is what bwrap does, so the behaviour is kept for now,
|
||||
// however recursively resolving links might improve user experience
|
||||
if rp, err := realpathHost(b.Source); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if b.Flags&BindOptional != 0 {
|
||||
return nil
|
||||
} else {
|
||||
return fmsg.WrapError(err,
|
||||
fmt.Sprintf("path %q does not exist", b.Source))
|
||||
}
|
||||
}
|
||||
return fmsg.WrapError(err, err.Error())
|
||||
} else {
|
||||
source = toHost(rp)
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(source); err != nil {
|
||||
return fmsg.WrapError(err, err.Error())
|
||||
} else if fi.IsDir() {
|
||||
if err = os.MkdirAll(target, 0755); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
fmt.Sprintf("cannot create directory %q:", b.Target))
|
||||
}
|
||||
} else if err = ensureFile(target, 0444); err != nil {
|
||||
if errors.Is(err, syscall.EISDIR) {
|
||||
return fmsg.WrapError(err,
|
||||
fmt.Sprintf("path %q is a directory", b.Target))
|
||||
}
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
fmt.Sprintf("cannot create %q:", b.Target))
|
||||
}
|
||||
|
||||
var flags uintptr = syscall.MS_SILENT | syscall.MS_BIND
|
||||
if b.Flags&BindRecursive != 0 {
|
||||
flags |= syscall.MS_REC
|
||||
}
|
||||
if b.Flags&BindWritable == 0 {
|
||||
flags |= syscall.MS_RDONLY
|
||||
}
|
||||
if b.Flags&BindDevices == 0 {
|
||||
flags |= syscall.MS_NODEV
|
||||
}
|
||||
if fmsg.Load() {
|
||||
if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
|
||||
fmsg.Verbosef("resolved %q flags %#x", target, flags)
|
||||
} else {
|
||||
fmsg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
|
||||
}
|
||||
}
|
||||
return fmsg.WrapErrorSuffix(syscall.Mount(source, target, "", flags, ""),
|
||||
fmt.Sprintf("cannot bind %q on %q:", b.Source, b.Target))
|
||||
}
|
||||
|
||||
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() error {
|
||||
if !path.IsAbs(p.Path) {
|
||||
return fmsg.WrapError(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", p.Path))
|
||||
}
|
||||
|
||||
target := toSysroot(p.Path)
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return fmsg.WrapError(err, err.Error())
|
||||
}
|
||||
return fmsg.WrapErrorSuffix(syscall.Mount("proc", target, "proc",
|
||||
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
|
||||
fmt.Sprintf("cannot mount proc on %q:", p.Path))
|
||||
}
|
||||
|
||||
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
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
func (t *MountTmpfs) apply() error {
|
||||
if !path.IsAbs(t.Path) {
|
||||
return fmsg.WrapError(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
}
|
||||
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||
return fmsg.WrapError(syscall.EBADE,
|
||||
fmt.Sprintf("size %d out of bounds", t.Size))
|
||||
}
|
||||
target := toSysroot(t.Path)
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
opt := fmt.Sprintf("mode=%#o", t.Mode)
|
||||
if t.Size > 0 {
|
||||
opt += fmt.Sprintf(",size=%d", t.Mode)
|
||||
}
|
||||
return fmsg.WrapErrorSuffix(syscall.Mount("tmpfs", target, "tmpfs",
|
||||
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
|
||||
fmt.Sprintf("cannot mount tmpfs on %q:", t.Path))
|
||||
}
|
||||
|
||||
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, mode os.FileMode) *Ops {
|
||||
*f = append(*f, &MountTmpfs{dest, size, mode})
|
||||
return f
|
||||
}
|
@ -73,6 +73,7 @@ buildGoModule rec {
|
||||
pkg-config
|
||||
wayland-scanner
|
||||
makeBinaryWrapper
|
||||
bubblewrap
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
|
Loading…
Reference in New Issue
Block a user