init: custom init process inside sandbox

Bubblewrap as init is a bit awkward and don't support a few setup actions fortify will need, such as starting/supervising nscd.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-10-14 02:27:02 +09:00
parent 315c9b8849
commit 1302bcede0
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
7 changed files with 222 additions and 24 deletions

View File

@ -90,6 +90,7 @@ func (s *SandboxConfig) Bwrap() *bwrap.Config {
Mqueue: []string{"/dev/mqueue"}, Mqueue: []string{"/dev/mqueue"},
NewSession: !s.NoNewSession, NewSession: !s.NoNewSession,
DieWithParent: true, DieWithParent: true,
AsInit: true,
} }
for _, c := range s.Filesystem { for _, c := range s.Filesystem {

View File

@ -29,17 +29,17 @@ func (a *app) Start() error {
defer a.lock.Unlock() defer a.lock.Unlock()
// resolve exec paths // resolve exec paths
e := [2]string{helper.BubblewrapName} shimExec := [3]string{a.seal.sys.executable, helper.BubblewrapName}
if len(a.seal.command) > 0 { if len(a.seal.command) > 0 {
e[1] = a.seal.command[0] shimExec[2] = a.seal.command[0]
} }
for i, n := range e { for i, n := range shimExec {
if len(n) == 0 { if len(n) == 0 {
continue continue
} }
if filepath.Base(n) == n { if filepath.Base(n) == n {
if s, err := exec.LookPath(n); err == nil { if s, err := exec.LookPath(n); err == nil {
e[i] = s shimExec[i] = s
} else { } else {
return (*ProcessError)(wrapError(err, fmt.Sprintf("cannot find %q: %v", n, err))) return (*ProcessError)(wrapError(err, fmt.Sprintf("cannot find %q: %v", n, err)))
} }
@ -72,7 +72,7 @@ func (a *app) Start() error {
if wls, err := shim.ServeConfig(confSockPath, &shim.Payload{ if wls, err := shim.ServeConfig(confSockPath, &shim.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: e, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,
WL: a.seal.wlDone != nil, WL: a.seal.wlDone != nil,
@ -105,7 +105,7 @@ func (a *app) Start() error {
err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) { err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) {
err.InnerErr = b.Save(&sd) err.InnerErr = b.Save(&sd)
}) })
return err.equiv("cannot save process state:", e) return err.equiv("cannot save process state:", err)
} }
// StateStoreError is returned for a failed state save // StateStoreError is returned for a failed state save

164
internal/init/main.go Normal file
View File

@ -0,0 +1,164 @@
package init0
import (
"encoding/gob"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
"path"
"strconv"
"syscall"
"git.ophivana.moe/cat/fortify/internal/verbose"
)
// everything beyond this point runs within pid namespace
// proceed with caution!
func doInit(fd uintptr) {
// re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil {
fmt.Println("fortify-init: cannot re-exec self:", err)
// continue anyway
}
}
verbose.Prefix = "fortify-init:"
var payload Payload
p := os.NewFile(fd, "config-stream")
if p == nil {
fmt.Println("fortify-init: invalid config descriptor")
os.Exit(1)
}
if err := gob.NewDecoder(p).Decode(&payload); err != nil {
fmt.Println("fortify-init: cannot decode init payload:", err)
os.Exit(1)
} else {
// sharing stdout with parent
// USE WITH CAUTION
verbose.Set(payload.Verbose)
// child does not need to see this
if err = os.Unsetenv(EnvInit); err != nil {
fmt.Println("fortify-init: cannot unset", EnvInit+":", err)
// not fatal
} else {
verbose.Println("received configuration")
}
}
// close config fd
if err := p.Close(); err != nil {
fmt.Println("fortify-init: cannot close config fd:", err)
// not fatal
}
// die with parent
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
fmt.Println("fortify-init: prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error())
os.Exit(1)
}
cmd := exec.Command(payload.Argv0)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = payload.Argv
cmd.Env = os.Environ()
// pass wayland fd
if payload.WL != -1 {
if f := os.NewFile(uintptr(payload.WL), "wayland"); f != nil {
cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(3+len(cmd.ExtraFiles)))
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
}
if err := cmd.Start(); err != nil {
fmt.Printf("fortify-init: cannot start %q: %v", payload.Argv0, err)
os.Exit(1)
}
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) {
fmt.Println("fortify-init: unexpected wait4 response:", err)
}
close(done)
}()
r := 2
for {
select {
case s := <-sig:
verbose.Println("received", s.String())
os.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
default:
r = 255
}
}
case <-done:
os.Exit(r)
}
}
}
// Try runs init and stops execution if FORTIFY_INIT is set.
func Try() {
if os.Getpid() != 1 {
return
}
if args := flag.Args(); len(args) == 1 && args[0] == "init" {
if s, ok := os.LookupEnv(EnvInit); ok {
if fd, err := strconv.Atoi(s); err != nil {
fmt.Printf("fortify-init: cannot parse %q: %v", s, err)
os.Exit(1)
} else {
doInit(uintptr(fd))
}
panic("unreachable")
}
}
}

15
internal/init/payload.go Normal file
View File

@ -0,0 +1,15 @@
package init0
const EnvInit = "FORTIFY_INIT"
type Payload struct {
// target full exec path
Argv0 string
// child full argv
Argv []string
// wayland fd, -1 to disable
WL int
// verbosity pass through
Verbose bool
}

View File

@ -12,6 +12,7 @@ import (
"syscall" "syscall"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/cat/fortify/helper"
init0 "git.ophivana.moe/cat/fortify/internal/init"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
@ -71,27 +72,24 @@ func doShim(socket string) {
// not fatal // not fatal
} }
var ic init0.Payload
// resolve argv0 // resolve argv0
var ( ic.Argv = payload.Argv
argv0 string if len(ic.Argv) > 0 {
argv = payload.Argv
)
if len(argv) > 0 {
// looked up from $PATH by parent // looked up from $PATH by parent
argv0 = payload.Exec[1] ic.Argv0 = payload.Exec[2]
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
if argv0, ok = os.LookupEnv("SHELL"); !ok { if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
fmt.Println("fortify-shim: no command was specified and $SHELL was unset") fmt.Println("fortify-shim: no command was specified and $SHELL was unset")
os.Exit(1) os.Exit(1)
} }
argv = []string{argv0} ic.Argv = []string{ic.Argv0}
} }
_ = conn.Close()
conf := payload.Bwrap conf := payload.Bwrap
var extraFiles []*os.File var extraFiles []*os.File
@ -99,13 +97,33 @@ func doShim(socket string) {
// pass wayland fd // pass wayland fd
if wfd != -1 { if wfd != -1 {
if f := os.NewFile(uintptr(wfd), "wayland"); f != nil { if f := os.NewFile(uintptr(wfd), "wayland"); f != nil {
conf.SetEnv["WAYLAND_SOCKET"] = strconv.Itoa(3 + len(extraFiles)) ic.WL = 3 + len(extraFiles)
extraFiles = append(extraFiles, f) extraFiles = append(extraFiles, f)
} }
} else {
ic.WL = -1
} }
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent // share config pipe
if b, err := helper.NewBwrap(conf, nil, argv0, func(_, _ int) []string { return argv[1:] }); err != nil { if r, w, err := os.Pipe(); err != nil {
fmt.Println("fortify-shim: cannot pipe:", err)
os.Exit(1)
} else {
conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
verbose.Println("transmitting config to init")
go func() {
// stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmt.Println("fortify-shim: cannot transmit init config:", err)
os.Exit(1)
}
}()
}
helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil {
fmt.Println("fortify-shim: malformed sandbox config:", err) fmt.Println("fortify-shim: malformed sandbox config:", err)
os.Exit(1) os.Exit(1)
} else { } else {

View File

@ -7,8 +7,8 @@ const EnvShim = "FORTIFY_SHIM"
type Payload struct { type Payload struct {
// child full argv // child full argv
Argv []string Argv []string
// bwrap, target full exec path // fortify, bwrap, target full exec path
Exec [2]string Exec [3]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// whether to pass wayland fd // whether to pass wayland fd

View File

@ -8,6 +8,7 @@ import (
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/cat/fortify/internal/app"
init0 "git.ophivana.moe/cat/fortify/internal/init"
"git.ophivana.moe/cat/fortify/internal/shim" "git.ophivana.moe/cat/fortify/internal/shim"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
@ -27,15 +28,14 @@ func main() {
// linux/sched/coredump.h // linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
fmt.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error()) fmt.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error())
} else {
verbose.Println("prctl(PR_SET_DUMPABLE, SUID_DUMP_DISABLE) succeeded")
} }
if internal.SdBootedV { if internal.SdBootedV {
verbose.Println("system booted with systemd as init system") verbose.Println("system booted with systemd as init system")
} }
// shim early exit // shim/init early exit
init0.Try()
shim.Try() shim.Try()
// root check // root check