aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/outcome/shim.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-29 04:32:43 +0900
committerOphestra <cat@gensokyo.uk>2025-10-29 04:33:13 +0900
commita0b4e47acc228e48165966d8e139277b8f6f450e (patch)
treed363a6781536ce946aa445271ca1d3df587e6031 /internal/outcome/shim.go
parenta52f7038e5a607dd0901244abb791c96deaf9c2d (diff)
internal/outcome: rename from app
This is less ambiguous, and more accurately describes the purpose of the package. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/outcome/shim.go')
-rw-r--r--internal/outcome/shim.go235
1 files changed, 235 insertions, 0 deletions
diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go
new file mode 100644
index 00000000..53d04ea2
--- /dev/null
+++ b/internal/outcome/shim.go
@@ -0,0 +1,235 @@
+package outcome
+
+import (
+ "context"
+ "errors"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "runtime"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ "hakurei.app/container"
+ "hakurei.app/container/comp"
+ "hakurei.app/container/seccomp"
+ "hakurei.app/hst"
+ "hakurei.app/message"
+)
+
+//#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"
+
+// shimParams is embedded in outcomeState and transmitted from priv side to shim.
+type shimParams struct {
+ // Priv side pid, checked against ppid in signal handler for the syscall.SIGCONT hack.
+ PrivPID int
+
+ // Duration to wait for after the initial process receives os.Interrupt before the container is killed.
+ // Limits are enforced on the priv side.
+ WaitDelay time.Duration
+
+ // Verbosity pass through from [message.Msg].
+ Verbose bool
+
+ // Outcome setup ops, contains setup state. Populated by outcome.finalise.
+ Ops []outcomeOp
+}
+
+// valid checks shimParams to be safe for use.
+func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
+
+// shimName is the prefix used by log.std in the shim process.
+const shimName = "shim"
+
+// 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 := k.receive(shimEnv, &state, nil); err != nil {
+ if errors.Is(err, syscall.EBADF) {
+ k.fatal("invalid config descriptor")
+ }
+ if errors.Is(err, container.ErrReceiveEnv) {
+ k.fatal(shimEnv + " not set")
+ }
+
+ k.fatalf("cannot receive shim setup params: %v", err)
+ } else {
+ msg.SwapVerbose(state.Shim.Verbose)
+ closeSetup = f
+
+ if err = state.populateLocal(k, msg); err != nil {
+ if m, ok := message.GetMessage(err); ok {
+ k.fatal(m)
+ } else {
+ 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, 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 wKeepAlive()
+ signalPipe = r
+ }
+
+ // pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
+ 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 {
+ k.fatal(m)
+ } else {
+ k.fatalf("cannot create container state: %v", err)
+ }
+ }
+ }
+
+ // shim exit outcomes
+ var cancelContainer atomic.Pointer[context.CancelFunc]
+ k.new(func(k syscallDispatcher, msg message.Msg) {
+ buf := make([]byte, 1)
+ for {
+ if _, err := signalPipe.Read(buf); err != nil {
+ k.fatalf("cannot read from signal pipe: %v", err)
+ }
+
+ switch buf[0] {
+ case 0: // got SIGCONT from monitor: shim exit requested
+ if fp := cancelContainer.Load(); stateParams.params.ForwardCancel && fp != nil && *fp != nil {
+ (*fp)()
+ // shim now bound by ShimWaitDelay, implemented below
+ continue
+ }
+
+ // setup has not completed, terminate immediately
+ msg.Resume()
+ k.exit(hst.ExitRequest)
+ return
+
+ case 1: // got SIGCONT after adoption: monitor died before delivering signal
+ msg.BeforeExit()
+ k.exit(hst.ExitOrphan)
+ return
+
+ case 2: // unreachable
+ msg.Verbose("sa_sigaction got invalid siginfo")
+
+ case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
+ msg.Verbose("got SIGCONT from unexpected process")
+
+ default: // unreachable
+ k.fatalf("got invalid message %d from signal handler", buf[0])
+ }
+ }
+ })
+
+ if stateParams.params.Ops == nil {
+ k.fatal("invalid container params")
+ }
+
+ // close setup socket
+ if err := closeSetup(); err != nil {
+ msg.Verbosef("cannot close setup pipe: %v", err)
+ // not fatal
+ }
+
+ ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ cancelContainer.Store(&stop)
+ z := container.New(ctx, msg)
+ z.Params = *stateParams.params
+ z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
+
+ // bounds and default enforced in finalise.go
+ z.WaitDelay = state.Shim.WaitDelay
+
+ if err := k.containerStart(z); err != nil {
+ printMessageError("cannot start container:", err)
+ k.exit(hst.ExitFailure)
+ }
+ if err := k.containerServe(z); err != nil {
+ printMessageError("cannot configure container:", err)
+ }
+
+ if err := k.seccompLoad(
+ seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch),
+ seccomp.AllowMultiarch,
+ ); err != nil {
+ k.fatalf("cannot load syscall filter: %v", err)
+ }
+
+ if err := k.containerWait(z); err != nil {
+ var exitError *exec.ExitError
+ if !errors.As(err, &exitError) {
+ if errors.Is(err, context.Canceled) {
+ k.exit(hst.ExitCancel)
+ }
+ msg.Verbosef("cannot wait: %v", err)
+ k.exit(127)
+ }
+ k.exit(exitError.ExitCode())
+ }
+}