From e5baaf416f1404a4a9ae8581603524d868716325 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 8 Oct 2025 19:48:04 +0900 Subject: [PATCH] internal/app: check transmitted ops This simulates params to shim and this is the last step before params to shim is merged. Signed-off-by: Ophestra --- internal/app/app_test.go | 91 +++++++++++++++++++++++++++++++++++-- internal/app/outcome.go | 2 - internal/app/spaccount.go | 3 ++ internal/app/spcontainer.go | 5 ++ internal/app/spdbus.go | 4 ++ internal/app/spfinal.go | 3 ++ internal/app/sppulse.go | 3 ++ internal/app/spruntime.go | 4 ++ internal/app/sptmpdir.go | 4 ++ internal/app/spwayland.go | 4 ++ internal/app/spx11.go | 3 ++ 11 files changed, 120 insertions(+), 6 deletions(-) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 90c4579..f40b092 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -2,11 +2,14 @@ package app import ( "bytes" + "encoding/gob" "encoding/json" + "errors" "fmt" "io" "io/fs" "log" + "maps" "os/exec" "os/user" "reflect" @@ -447,21 +450,101 @@ func TestApp(t *testing.T) { err := seal.finalise(t.Context(), msg, &tc.id, tc.config) if err != nil { if s, ok := container.GetErrorMessage(err); !ok { - t.Fatalf("Seal: error = %v", err) + t.Fatalf("outcome: error = %v", err) } else { - t.Fatalf("Seal: %s", s) + t.Fatalf("outcome: %s", s) } } t.Run("sys", func(t *testing.T) { if !seal.sys.Equal(tc.wantSys) { - t.Errorf("Seal: sys = %#v, want %#v", seal.sys, tc.wantSys) + t.Errorf("outcome: sys = %#v, want %#v", seal.sys, tc.wantSys) } }) t.Run("params", func(t *testing.T) { if !reflect.DeepEqual(&seal.container, tc.wantParams) { - t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(&seal.container), mustMarshal(tc.wantParams)) + t.Errorf("outcome: container =\n%s\n, want\n%s", mustMarshal(&seal.container), mustMarshal(tc.wantParams)) + } + }) + }) + + t.Run("ops", func(t *testing.T) { + // copied from shim + const envAllocSize = 1 << 6 + + gr, gw := io.Pipe() + + var gotSys *system.I + { + sPriv := outcomeState{ + ID: &tc.id, + Identity: tc.config.Identity, + UserID: (&Hsu{k: tc.k}).MustIDMsg(msg), + EnvPaths: copyPaths(tc.k), + Container: tc.config.Container, + } + + sPriv.populateEarly(tc.k, msg) + if err := sPriv.populateLocal(tc.k, msg); err != nil { + t.Fatalf("populateLocal: error = %#v", err) + } + + gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap()) + opsPriv := fromConfig(tc.config) + stateSys := outcomeStateSys{sys: gotSys, outcomeState: &sPriv} + for _, op := range opsPriv { + if err := op.toSystem(&stateSys, tc.config); err != nil { + t.Fatalf("toSystem: error = %#v", err) + } + } + + go func() { + e := gob.NewEncoder(gw) + if err := errors.Join(e.Encode(&sPriv), e.Encode(&opsPriv)); err != nil { + t.Errorf("Encode: error = %v", err) + panic("unexpected encode fault") + } + }() + } + + var gotParams container.Params + { + var ( + sShim outcomeState + opsShim []outcomeOp + ) + + d := gob.NewDecoder(gr) + if err := errors.Join(d.Decode(&sShim), d.Decode(&opsShim)); err != nil { + t.Fatalf("Decode: error = %v", err) + } + if err := sShim.populateLocal(tc.k, msg); err != nil { + t.Fatalf("populateLocal: error = %#v", err) + } + + stateParams := outcomeStateParams{params: &gotParams, outcomeState: &sShim} + if sShim.Container.Env == nil { + stateParams.env = make(map[string]string, envAllocSize) + } else { + stateParams.env = maps.Clone(sShim.Container.Env) + } + for _, op := range opsShim { + if err := op.toContainer(&stateParams); err != nil { + t.Fatalf("toContainer: error = %#v", err) + } + } + } + + t.Run("sys", func(t *testing.T) { + if !gotSys.Equal(tc.wantSys) { + t.Errorf("toSystem: sys = %#v, want %#v", gotSys, tc.wantSys) + } + }) + + t.Run("params", func(t *testing.T) { + if !reflect.DeepEqual(&gotParams, tc.wantParams) { + t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(&gotParams), mustMarshal(tc.wantParams)) } }) }) diff --git a/internal/app/outcome.go b/internal/app/outcome.go index 671d14c..ef7c780 100644 --- a/internal/app/outcome.go +++ b/internal/app/outcome.go @@ -194,8 +194,6 @@ type outcomeStateParams struct { *outcomeState } -// TODO(ophestra): register outcomeOp implementations (params to shim) - // An outcomeOp inflicts an outcome on [system.I] and contains enough information to // inflict it on [container.Params] in a separate process. // An implementation of outcomeOp must store cross-process states in exported fields only. diff --git a/internal/app/spaccount.go b/internal/app/spaccount.go index 0927bcc..6976e80 100644 --- a/internal/app/spaccount.go +++ b/internal/app/spaccount.go @@ -1,6 +1,7 @@ package app import ( + "encoding/gob" "fmt" "syscall" @@ -8,6 +9,8 @@ import ( "hakurei.app/hst" ) +func init() { gob.Register(spAccountOp{}) } + // spAccountOp sets up user account emulation inside the container. type spAccountOp struct{} diff --git a/internal/app/spcontainer.go b/internal/app/spcontainer.go index bce0e20..3827fa2 100644 --- a/internal/app/spcontainer.go +++ b/internal/app/spcontainer.go @@ -1,6 +1,7 @@ package app import ( + "encoding/gob" "errors" "io/fs" "os" @@ -19,6 +20,8 @@ import ( const varRunNscd = fhs.Var + "run/nscd" +func init() { gob.Register(new(spParamsOp)) } + // spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem. // This outcomeOp is hardcoded to always run first. type spParamsOp struct { @@ -113,6 +116,8 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error { return nil } +func init() { gob.Register(spFilesystemOp{}) } + // spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem. type spFilesystemOp struct{} diff --git a/internal/app/spdbus.go b/internal/app/spdbus.go index 77c2dcd..81e0595 100644 --- a/internal/app/spdbus.go +++ b/internal/app/spdbus.go @@ -1,12 +1,16 @@ package app import ( + "encoding/gob" + "hakurei.app/container/fhs" "hakurei.app/hst" "hakurei.app/system/acl" "hakurei.app/system/dbus" ) +func init() { gob.Register(new(spDBusOp)) } + // spDBusOp maintains an xdg-dbus-proxy instance for the container. type spDBusOp struct { // Whether to bind the system bus socket. diff --git a/internal/app/spfinal.go b/internal/app/spfinal.go index c825508..b64d080 100644 --- a/internal/app/spfinal.go +++ b/internal/app/spfinal.go @@ -1,6 +1,7 @@ package app import ( + "encoding/gob" "fmt" "slices" "strings" @@ -12,6 +13,8 @@ import ( "hakurei.app/system/acl" ) +func init() { gob.Register(spFinal{}) } + // spFinal is a transitional op destined for removal after #3, #8, #9 has been resolved. // It exists to avoid reordering the expected entries in test cases. type spFinal struct{} diff --git a/internal/app/sppulse.go b/internal/app/sppulse.go index a6dba59..bf79ae5 100644 --- a/internal/app/sppulse.go +++ b/internal/app/sppulse.go @@ -1,6 +1,7 @@ package app import ( + "encoding/gob" "errors" "fmt" "io" @@ -14,6 +15,8 @@ import ( const pulseCookieSizeMax = 1 << 8 +func init() { gob.Register(new(spPulseOp)) } + // spPulseOp exports the PulseAudio server to the container. type spPulseOp struct { // PulseAudio cookie data, populated during toSystem if a cookie is present. diff --git a/internal/app/spruntime.go b/internal/app/spruntime.go index d327003..87decd1 100644 --- a/internal/app/spruntime.go +++ b/internal/app/spruntime.go @@ -1,6 +1,8 @@ package app import ( + "encoding/gob" + "hakurei.app/container/bits" "hakurei.app/container/check" "hakurei.app/container/fhs" @@ -9,6 +11,8 @@ import ( "hakurei.app/system/acl" ) +func init() { gob.Register(spRuntimeOp{}) } + // spRuntimeOp sets up XDG_RUNTIME_DIR inside the container. type spRuntimeOp struct{} diff --git a/internal/app/sptmpdir.go b/internal/app/sptmpdir.go index 4cc0c7d..8f8dd85 100644 --- a/internal/app/sptmpdir.go +++ b/internal/app/sptmpdir.go @@ -1,6 +1,8 @@ package app import ( + "encoding/gob" + "hakurei.app/container/bits" "hakurei.app/container/check" "hakurei.app/container/fhs" @@ -9,6 +11,8 @@ import ( "hakurei.app/system/acl" ) +func init() { gob.Register(spTmpdirOp{}) } + // spTmpdirOp sets up TMPDIR inside the container. type spTmpdirOp struct{} diff --git a/internal/app/spwayland.go b/internal/app/spwayland.go index 91ce1ac..efbd8af 100644 --- a/internal/app/spwayland.go +++ b/internal/app/spwayland.go @@ -1,12 +1,16 @@ package app import ( + "encoding/gob" + "hakurei.app/container/check" "hakurei.app/hst" "hakurei.app/system/acl" "hakurei.app/system/wayland" ) +func init() { gob.Register(new(spWaylandOp)) } + // spWaylandOp exports the Wayland display server to the container. type spWaylandOp struct { // Path to host wayland socket. Populated during toSystem if DirectWayland is true. diff --git a/internal/app/spx11.go b/internal/app/spx11.go index 18890fd..ba7a768 100644 --- a/internal/app/spx11.go +++ b/internal/app/spx11.go @@ -1,6 +1,7 @@ package app import ( + "encoding/gob" "errors" "fmt" "io/fs" @@ -15,6 +16,8 @@ import ( var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix") +func init() { gob.Register(new(spX11Op)) } + // spX11Op exports the X11 display server to the container. type spX11Op struct { // Value of $DISPLAY, stored during toSystem