internal/app/shim: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m28s

This enables instrumented testing of the shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-10-22 06:55:02 +09:00
parent c5f59c5488
commit 8accd3b219
7 changed files with 190 additions and 75 deletions

View File

@@ -7,7 +7,6 @@ import (
"log"
"os"
"os/exec"
"os/signal"
"runtime"
"sync/atomic"
"syscall"
@@ -23,6 +22,20 @@ import (
//#include "shim-signal.h"
import "C"
// setupContSignal sets up the SIGCONT signal handler for the cross-uid shim exit hack.
// The signal handler is implemented in C, signals can be processed by reading from the returned reader.
// The returned function must be called after all signal processing concludes.
func setupContSignal(pid int) (io.ReadCloser, func(), error) {
if r, w, err := os.Pipe(); err != nil {
return nil, nil, err
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(pid), C.int(w.Fd())); err != nil {
_, _ = r.Close(), w.Close()
return nil, nil, err
} else {
return r, func() { runtime.KeepAlive(w) }, nil
}
}
// shimEnv is the name of the environment variable storing decimal representation of
// setup pipe fd for [container.Receive].
const shimEnv = "HAKUREI_SHIM"
@@ -46,76 +59,102 @@ type shimParams struct {
// valid checks shimParams to be safe for use.
func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() {
log.SetPrefix("shim: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
// shimName is the prefix used by log.std in the shim process.
const shimName = "shim"
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
// Shim is called by the main function of the shim process and runs as the unconstrained target user.
// Shim does not return.
func Shim(msg message.Msg) {
if msg == nil {
msg = message.NewMsg(log.Default())
}
shimEntrypoint(direct{msg})
}
func shimEntrypoint(k syscallDispatcher) {
msg := k.getMsg()
if msg == nil {
panic("attempting to call shimEntrypoint with nil msg")
} else if logger := msg.GetLogger(); logger != nil {
logger.SetPrefix(shimName + ": ")
logger.SetFlags(0)
}
if err := k.setDumpable(container.SUID_DUMP_DISABLE); err != nil {
k.fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
var (
state outcomeState
closeSetup func() error
)
if f, err := container.Receive(shimEnv, &state, nil); err != nil {
if f, err := k.receive(shimEnv, &state, nil); err != nil {
if errors.Is(err, syscall.EBADF) {
log.Fatal("invalid config descriptor")
k.fatal("invalid config descriptor")
}
if errors.Is(err, container.ErrReceiveEnv) {
log.Fatal(shimEnv + " not set")
k.fatal(shimEnv + " not set")
}
log.Fatalf("cannot receive shim setup params: %v", err)
k.fatalf("cannot receive shim setup params: %v", err)
} else {
msg.SwapVerbose(state.Shim.Verbose)
closeSetup = f
if err = state.populateLocal(direct{}, msg); err != nil {
if err = state.populateLocal(k, msg); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Fatal(m)
k.fatal(m)
} else {
log.Fatalf("cannot populate local state: %v", err)
k.fatalf("cannot populate local state: %v", err)
}
}
}
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
var signalPipe io.ReadCloser
if r, w, err := os.Pipe(); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(state.Shim.PrivPID), C.int(w.Fd())); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
if r, wKeepAlive, err := k.setupContSignal(state.Shim.PrivPID); err != nil {
switch {
case errors.As(err, new(*os.SyscallError)): // returned by os.Pipe
k.fatal(err.Error())
return
case errors.As(err, new(syscall.Errno)): // returned by hakurei_shim_setup_cont_signal
k.fatalf("cannot install SIGCONT handler: %v", err)
return
default: // unreachable
k.fatalf("cannot set up exit request: %v", err)
return
}
} else {
defer runtime.KeepAlive(w)
defer wKeepAlive()
signalPipe = r
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
if err := k.prctl(syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); err != nil {
k.fatalf("cannot set parent-death signal: %v", err)
}
stateParams := state.newParams()
for _, op := range state.Shim.Ops {
if err := op.toContainer(stateParams); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Fatal(m)
k.fatal(m)
} else {
log.Fatalf("cannot create container state: %v", err)
k.fatalf("cannot create container state: %v", err)
}
}
}
// shim exit outcomes
var cancelContainer atomic.Pointer[context.CancelFunc]
go func() {
k.new(func(k syscallDispatcher, msg message.Msg) {
buf := make([]byte, 1)
for {
if _, err := signalPipe.Read(buf); err != nil {
log.Fatalf("cannot read from signal pipe: %v", err)
k.fatalf("cannot read from signal pipe: %v", err)
}
switch buf[0] {
@@ -128,37 +167,37 @@ func ShimMain() {
// setup has not completed, terminate immediately
msg.Resume()
os.Exit(hst.ExitRequest)
k.exit(hst.ExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
msg.BeforeExit()
os.Exit(hst.ExitOrphan)
k.exit(hst.ExitOrphan)
return
case 2: // unreachable
log.Println("sa_sigaction got invalid siginfo")
msg.Verbose("sa_sigaction got invalid siginfo")
case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
log.Println("got SIGCONT from unexpected process")
msg.Verbose("got SIGCONT from unexpected process")
default: // unreachable
log.Fatalf("got invalid message %d from signal handler", buf[0])
k.fatalf("got invalid message %d from signal handler", buf[0])
}
}
}()
})
if stateParams.params.Ops == nil {
log.Fatal("invalid container params")
k.fatal("invalid container params")
}
// close setup socket
if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err)
msg.Verbosef("cannot close setup pipe: %v", err)
// not fatal
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop)
z := container.New(ctx, msg)
z.Params = *stateParams.params
@@ -167,30 +206,30 @@ func ShimMain() {
// bounds and default enforced in finalise.go
z.WaitDelay = state.Shim.WaitDelay
if err := z.Start(); err != nil {
if err := k.containerStart(z); err != nil {
printMessageError("cannot start container:", err)
os.Exit(hst.ExitFailure)
k.exit(hst.ExitFailure)
}
if err := z.Serve(); err != nil {
if err := k.containerServe(z); err != nil {
printMessageError("cannot configure container:", err)
}
if err := seccomp.Load(
if err := k.seccompLoad(
seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch),
seccomp.AllowMultiarch,
); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
k.fatalf("cannot load syscall filter: %v", err)
}
if err := z.Wait(); err != nil {
if err := k.containerWait(z); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(hst.ExitCancel)
k.exit(hst.ExitCancel)
}
log.Printf("wait: %v", err)
os.Exit(127)
msg.Verbosef("cannot wait: %v", err)
k.exit(127)
}
os.Exit(exitError.ExitCode())
k.exit(exitError.ExitCode())
}
}