From 11962001216846977fc522066f4a80a06c30f9b1 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Fri, 31 Oct 2025 22:34:38 +0900 Subject: [PATCH] container/params: expose pipe This increases flexibility of how caller wants to handle the I/O. Also makes it no longer rely on finalizer. Signed-off-by: Ophestra --- container/container.go | 31 ++++++++++++++++------------ container/params.go | 4 ++-- container/params_test.go | 9 +++++++-- internal/outcome/process.go | 40 +++++++++++++++++-------------------- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/container/container.go b/container/container.go index 55e4c44..3ba87da 100644 --- a/container/container.go +++ b/container/container.go @@ -25,6 +25,9 @@ const ( // CancelSignal is the signal expected by container init on context cancel. // A custom [Container.Cancel] function must eventually deliver this signal. CancelSignal = SIGUSR2 + + // Timeout for writing initParams to Container.setup. + initSetupTimeout = 5 * time.Second ) type ( @@ -37,8 +40,8 @@ type ( // with behaviour identical to its [exec.Cmd] counterpart. ExtraFiles []*os.File - // param encoder for shim and init - setup *gob.Encoder + // param pipe for shim and init + setup *os.File // cancels cmd cancel context.CancelFunc // closed after Wait returns @@ -228,10 +231,10 @@ func (p *Container) Start() error { } // place setup pipe before user supplied extra files, this is later restored by init - if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { + if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil { return &StartError{true, "set up params stream", err, false, false} } else { - p.setup = e + p.setup = f p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} } p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) @@ -310,6 +313,9 @@ func (p *Container) Serve() error { setup := p.setup p.setup = nil + if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil { + return &StartError{true, "set init pipe deadline", err, false, true} + } if p.Path == nil { p.cancel() @@ -324,15 +330,14 @@ func (p *Container) Serve() error { p.SeccompRules = make([]seccomp.NativeRule, 0) } - err := setup.Encode( - &initParams{ - p.Params, - Getuid(), - Getgid(), - len(p.ExtraFiles), - p.msg.IsVerbose(), - }, - ) + err := gob.NewEncoder(setup).Encode(&initParams{ + p.Params, + Getuid(), + Getgid(), + len(p.ExtraFiles), + p.msg.IsVerbose(), + }) + _ = setup.Close() if err != nil { p.cancel() } diff --git a/container/params.go b/container/params.go index a48eaba..46c3b9b 100644 --- a/container/params.go +++ b/container/params.go @@ -9,13 +9,13 @@ import ( ) // Setup appends the read end of a pipe for setup params transmission and returns its fd. -func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { +func Setup(extraFiles *[]*os.File) (int, *os.File, error) { if r, w, err := os.Pipe(); err != nil { return -1, nil, err } else { fd := 3 + len(*extraFiles) *extraFiles = append(*extraFiles, r) - return fd, gob.NewEncoder(w), nil + return fd, w, nil } } diff --git a/container/params_test.go b/container/params_test.go index a65f686..f74abf1 100644 --- a/container/params_test.go +++ b/container/params_test.go @@ -1,6 +1,7 @@ package container_test import ( + "encoding/gob" "errors" "os" "slices" @@ -59,12 +60,16 @@ func TestSetupReceive(t *testing.T) { encoderDone := make(chan error, 1) extraFiles := make([]*os.File, 0, 1) - if fd, encoder, err := container.Setup(&extraFiles); err != nil { + deadline, _ := t.Deadline() + if fd, f, err := container.Setup(&extraFiles); err != nil { t.Fatalf("Setup: error = %v", err) } else if fd != 3 { t.Fatalf("Setup: fd = %d, want 3", fd) } else { - go func() { encoderDone <- encoder.Encode(payload) }() + if err = f.SetDeadline(deadline); err != nil { + t.Fatal(err.Error()) + } + go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }() } if len(extraFiles) != 1 { diff --git a/internal/outcome/process.go b/internal/outcome/process.go index 4ef49b0..4fbfdab 100644 --- a/internal/outcome/process.go +++ b/internal/outcome/process.go @@ -20,8 +20,12 @@ import ( "hakurei.app/system" ) -// Duration to wait for shim to exit on top of container WaitDelay. -const shimWaitTimeout = 5 * time.Second +const ( + // Duration to wait for shim to exit on top of container WaitDelay. + shimWaitTimeout = 5 * time.Second + // Timeout from setup pipe creation to when outcomeState is fully written. + shimSetupTimeout = 5 * time.Second +) // mainState holds persistent state bound to outcome.main. type mainState struct { @@ -214,7 +218,7 @@ func (k *outcome) main(msg message.Msg) { hsuPath := internal.MustHsuPath() // ms.beforeExit required beyond this point - ms := &mainState{Msg: msg, k: k} + ms := mainState{Msg: msg, k: k} if err := k.sys.Commit(); err != nil { ms.fatal("cannot commit system setup:", err) @@ -232,11 +236,12 @@ func (k *outcome) main(msg message.Msg) { // shim runs in the same session as monitor; see shim.go for behaviour ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) } - var e *gob.Encoder - if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil { + var shimPipe *os.File + if fd, w, err := container.Setup(&ms.cmd.ExtraFiles); err != nil { ms.fatal("cannot create shim setup pipe:", err) + panic("unreachable") } else { - e = encoder + shimPipe = w ms.cmd.Env = []string{ // passed through to shim by hsu shimEnv + "=" + strconv.Itoa(fd), @@ -262,23 +267,14 @@ func (k *outcome) main(msg message.Msg) { go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }() ms.Time = &startTime - // unfortunately the I/O here cannot be directly canceled; - // the cancellation path leads to fatal in this case so that is fine - select { - case err := <-func() (setupErr chan error) { - setupErr = make(chan error, 1) - go func() { setupErr <- e.Encode(k.state) }() - return - }(): - if err != nil { - msg.Resume() - ms.fatal("cannot transmit shim config:", err) - } - - case <-ctx.Done(): - msg.Resume() - ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err())) + if err := shimPipe.SetDeadline(time.Now().Add(shimSetupTimeout)); err != nil { + msg.Verbose(err.Error()) } + if err := gob.NewEncoder(shimPipe).Encode(k.state); err != nil { + msg.Resume() + ms.fatal("cannot transmit shim config:", err) + } + _ = shimPipe.Close() // shim accepted setup payload, create process state if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c store.Cursor) {