From f1db0b5004674c762c8195a8d64c1de730b6aca6 Mon Sep 17 00:00:00 2001 From: dawn Date: Sun, 26 Jul 2026 22:38:37 +0000 Subject: [PATCH] spindle/microvm: key the activation config cache per repo so tenants can't poison each other's toplevels Signed-off-by: dawn --- cmd/spindle-microvm-run/main_linux.go | 2 +- spindle/engines/microvm/cid_test.go | 30 ++++++++++++++++++++++++++++++ spindle/engines/microvm/engine.go | 9 ++++++++- spindle/engines/microvm/nixos_toplevel_cache.go | 11 +++++++---- 4 file(s) changed, 46 insertion(s)(+), 6 deletion(s)(-) diff --git a/cmd/spindle-microvm-run/main_linux.go b/cmd/spindle-microvm-run/main_linux.go --- a/cmd/spindle-microvm-run/main_linux.go +++ b/cmd/spindle-microvm-run/main_linux.go @@ -257,7 +257,7 @@ return fmt.Errorf("failed to open database: %w", err) } defer configCacheDB.Close() - configKey, err = microvm.BuildConfigKey(imageSpec, cmd.String("activate-config")) + configKey, err = microvm.BuildConfigKey(imageSpec, cmd.String("activate-config"), "local") if err != nil { return fmt.Errorf("calculate config key: %w", err) } diff --git a/spindle/engines/microvm/cid_test.go b/spindle/engines/microvm/cid_test.go new file mode 100644 --- /dev/null +++ b/spindle/engines/microvm/cid_test.go @@ -0,0 +1,30 @@ +package microvm + +import ( + "testing" +) + +func TestBuildConfigKeyScopesByRepo(t *testing.T) { + spec := ImageSpec{BaseConfigHash: "deadbeef"} + cfg := manifestConfig{Dependencies: []string{"nodejs"}} + + a, err := buildConfigKey(spec, cfg, "did:plc:aaa") + if err != nil { + t.Fatal(err) + } + b, err := buildConfigKey(spec, cfg, "did:plc:bbb") + if err != nil { + t.Fatal(err) + } + if a == b { + t.Errorf("same config in different repos produced the same key %q", a) + } + + again, err := buildConfigKey(spec, cfg, "did:plc:aaa") + if err != nil { + t.Fatal(err) + } + if a != again { + t.Errorf("key not deterministic: %q vs %q", a, again) + } +} diff --git a/spindle/engines/microvm/engine.go b/spindle/engines/microvm/engine.go --- a/spindle/engines/microvm/engine.go +++ b/spindle/engines/microvm/engine.go @@ -160,8 +160,15 @@ "microVM image %q is not a NixOS image: services, virtualisation, dependencies and registry workflow options require a NixOS image", imageName, ) } + // the cached toplevel is guest-asserted, so the cache key has to + // be per-repo. a global key lets one tenant poison the activation + // of another tenant's identical config + repoDid := "" + if md := tpl.TriggerMetadata; md != nil && md.Repo != nil { + repoDid = md.Repo.Did + } var err error - configKey, err = buildConfigKey(imageSpec, config) + configKey, err = buildConfigKey(imageSpec, config, repoDid) if err != nil { return nil, fmt.Errorf("build config key: %w", err) } diff --git a/spindle/engines/microvm/nixos_toplevel_cache.go b/spindle/engines/microvm/nixos_toplevel_cache.go --- a/spindle/engines/microvm/nixos_toplevel_cache.go +++ b/spindle/engines/microvm/nixos_toplevel_cache.go @@ -12,7 +12,8 @@ "tangled.org/core/spindle/db" ) -const nixosToplevelCacheSchemaVersion = 1 +// v2 keys are scoped by repo did in v1 keys were global +const nixosToplevelCacheSchemaVersion = 2 type nixosToplevelCacheRecord struct { ConfigKey string `json:"config_key"` @@ -72,7 +73,7 @@ sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } -func buildConfigKey(imageSpec ImageSpec, cfg manifestConfig) (string, error) { +func buildConfigKey(imageSpec ImageSpec, cfg manifestConfig, repoDid string) (string, error) { baseHash, err := BaseConfigHash(imageSpec) if err != nil { return "", err @@ -81,20 +82,22 @@ payload := struct { Schema int `json:"schema"` BaseConfig string `json:"base_config"` UserConfig string `json:"user_config"` + RepoDid string `json:"repo_did"` }{ Schema: nixosToplevelCacheSchemaVersion, BaseConfig: baseHash, UserConfig: userConfigHash(cfg), + RepoDid: repoDid, } data, _ := json.Marshal(payload) sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]), nil } -func BuildConfigKey(imageSpec ImageSpec, userConfigJSON string) (string, error) { +func BuildConfigKey(imageSpec ImageSpec, userConfigJSON string, repoDid string) (string, error) { var cfg manifestConfig if err := json.Unmarshal([]byte(userConfigJSON), &cfg); err != nil { return "", err } - return buildConfigKey(imageSpec, cfg) + return buildConfigKey(imageSpec, cfg, repoDid) } -- tangled.sh