ollie/cmd/toolsrv/internal/sandbox/native_linux.go

118 lines
3.7 KiB
Go

//go:build linux
package sandbox
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
type helperPolicy struct {
Config Config `json:"config"`
CWD string `json:"cwd"`
Env map[string]string `json:"env"`
Command []string `json:"command"`
}
func ExecHelper(args []string) error {
if len(args) != 1 {
return fmt.Errorf("sandbox-exec: expected encoded policy")
}
data, err := base64.RawStdEncoding.DecodeString(args[0])
if err != nil {
return err
}
var p helperPolicy
if err := json.Unmarshal(data, &p); err != nil {
return err
}
if err := restrict(&p.Config, p.CWD, p.Env); err != nil {
return err
}
if len(p.Command) == 0 {
return fmt.Errorf("sandbox-exec: empty command")
}
if err := syscall.Chdir(p.CWD); err != nil {
return err
}
// syscall.Exec requires an absolute path; resolve via PATH if needed
cmd0 := p.Command[0]
if !strings.HasPrefix(cmd0, "/") {
if resolved, err := exec.LookPath(cmd0); err == nil {
cmd0 = resolved
}
}
return syscall.Exec(cmd0, p.Command, envList(p.Env))
}
func envList(env map[string]string) []string {
out := make([]string, 0, len(env))
for k, v := range env {
out = append(out, k+"="+v)
}
return out
}
func restrict(cfg *Config, cwd string, env map[string]string) error {
if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
return fmt.Errorf("set no_new_privs: %w", err)
}
fsAccess := uint64(unix.LANDLOCK_ACCESS_FS_EXECUTE | unix.LANDLOCK_ACCESS_FS_WRITE_FILE | unix.LANDLOCK_ACCESS_FS_READ_FILE | unix.LANDLOCK_ACCESS_FS_READ_DIR | unix.LANDLOCK_ACCESS_FS_REMOVE_DIR | unix.LANDLOCK_ACCESS_FS_REMOVE_FILE | unix.LANDLOCK_ACCESS_FS_MAKE_CHAR | unix.LANDLOCK_ACCESS_FS_MAKE_DIR | unix.LANDLOCK_ACCESS_FS_MAKE_REG | unix.LANDLOCK_ACCESS_FS_MAKE_SOCK | unix.LANDLOCK_ACCESS_FS_MAKE_FIFO | unix.LANDLOCK_ACCESS_FS_MAKE_BLOCK | unix.LANDLOCK_ACCESS_FS_MAKE_SYM)
fd, _, err := unix.Syscall(unix.SYS_LANDLOCK_CREATE_RULESET, uintptr(unsafe.Pointer(&fsAccess)), unsafe.Sizeof(fsAccess), 0)
if err != 0 {
return fmt.Errorf("landlock unavailable: %w", err)
}
defer unix.Close(int(fd))
add := func(paths []string, access uint64) error {
for _, pattern := range paths {
p := expandPath(pattern, cwd, func(k string) string { return env[k] })
if !pathExists(p) {
continue
}
f, e := unix.Open(p, unix.O_PATH|unix.O_CLOEXEC, 0)
if e != nil {
return e
}
rule := struct {
Allowed uint64
ParentFD int
}{access, int(f)}
_, _, errno := unix.Syscall6(unix.SYS_LANDLOCK_ADD_RULE, fd, uintptr(1), uintptr(unsafe.Pointer(&rule)), 0, 0, 0)
unix.Close(f)
if errno != 0 {
return fmt.Errorf("add landlock path %s: %w", p, errno)
}
}
return nil
}
read := uint64(unix.LANDLOCK_ACCESS_FS_READ_FILE | unix.LANDLOCK_ACCESS_FS_READ_DIR)
write := uint64(unix.LANDLOCK_ACCESS_FS_WRITE_FILE | unix.LANDLOCK_ACCESS_FS_REMOVE_DIR | unix.LANDLOCK_ACCESS_FS_REMOVE_FILE | unix.LANDLOCK_ACCESS_FS_MAKE_DIR | unix.LANDLOCK_ACCESS_FS_MAKE_REG | unix.LANDLOCK_ACCESS_FS_MAKE_SOCK | unix.LANDLOCK_ACCESS_FS_MAKE_FIFO | unix.LANDLOCK_ACCESS_FS_MAKE_CHAR | unix.LANDLOCK_ACCESS_FS_MAKE_BLOCK | unix.LANDLOCK_ACCESS_FS_MAKE_SYM)
if err := add(cfg.Filesystem.RO, read); err != nil {
return err
}
if err := add(cfg.Filesystem.ROX, read|unix.LANDLOCK_ACCESS_FS_EXECUTE); err != nil {
return err
}
if err := add(cfg.Filesystem.RW, read|write); err != nil {
return err
}
if err := add(cfg.Filesystem.RWX, read|write|unix.LANDLOCK_ACCESS_FS_EXECUTE); err != nil {
return err
}
_, _, err = unix.Syscall(unix.SYS_LANDLOCK_RESTRICT_SELF, fd, 0, 0)
if err != 0 {
return fmt.Errorf("restrict landlock: %w", err)
}
return nil
}
var _ = os.Args