From e2ba25eb4811b1e48044934ff688dc933edbf330 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 16 Oct 2026 12:49:03 -0500 Subject: [PATCH] fixes nil config flakes for spec and runtimeoptions Signed-off-by: Mike Brown --- core/metadata/containers.go | 7 +++++- core/metadata/containers_test.go | 38 ++++++++++++++++++++++++++++++++ core/metadata/sandbox.go | 12 ++++++++-- core/metadata/sandbox_test.go | 37 +++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/core/metadata/containers.go b/core/metadata/containers.go index 9d72b5086f..1a15a3a1ef 100644 --- a/core/metadata/containers.go +++ b/core/metadata/containers.go @@ -383,7 +383,12 @@ func readContainer(container *containers.Container, bkt *bolt.Bucket) error { if err != nil { return err } - container.Runtime.Options = o + // Avoid assigning a typed-nil (*anypb.Any)(nil) to the typeurl.Any + // interface, which would produce a non-nil interface value that + // fails type-URL lookup. + if o != nil { + container.Runtime.Options = o + } case string(bucketKeySpec): var spec types.Any if err := proto.Unmarshal(v, &spec); err != nil { diff --git a/core/metadata/containers_test.go b/core/metadata/containers_test.go index 077bf84f26..994eaa9483 100644 --- a/core/metadata/containers_test.go +++ b/core/metadata/containers_test.go @@ -721,3 +721,41 @@ func testEnv(t *testing.T) (context.Context, *bolt.DB) { return ctx, db } + +// TestContainerNilRuntimeOptionsRoundTrip verifies that a container stored +// without Runtime.Options round-trips back with a genuinely nil interface +// value, not a typed-nil *types.Any wrapped in a typeurl.Any interface. +// +// checkContainersEqual uses compareNil, which treats a typed-nil pointer +// wrapped in an interface as equal to a true nil interface and therefore would +// not catch this regression. A non-nil interface with a nil underlying pointer +// causes type-URL lookup failures at runtime. +func TestContainerNilRuntimeOptionsRoundTrip(t *testing.T) { + ctx, db := testEnv(t) + store := NewContainerStore(NewDB(db, nil, nil)) + + spec := &specs.Spec{} + encoded, err := typeurl.MarshalAnyToProto(spec) + require.NoError(t, err) + + in := containers.Container{ + ID: "nil-runtime-opts", + Spec: encoded, + Runtime: containers.RuntimeInfo{ + Name: "testruntime", + // Options intentionally left nil. + }, + } + + _, err = store.Create(ctx, in) + require.NoError(t, err) + + out, err := store.Get(ctx, "nil-runtime-opts") + require.NoError(t, err) + + // Use direct interface comparison — not compareNil — to distinguish a + // true nil interface from a typed-nil pointer wrapped in an interface. + if out.Runtime.Options != nil { + t.Errorf("Runtime.Options: want nil interface, got %T(%v)", out.Runtime.Options, out.Runtime.Options) + } +} diff --git a/core/metadata/sandbox.go b/core/metadata/sandbox.go index c97010a006..bf20c38b21 100644 --- a/core/metadata/sandbox.go +++ b/core/metadata/sandbox.go @@ -371,10 +371,15 @@ func (s *sandboxStore) read(parent *bbolt.Bucket, id []byte) (api.Sandbox, error return api.Sandbox{}, err } - inst.Spec, err = boltutil.ReadAny(bucket, bucketKeySpec) + spec, err := boltutil.ReadAny(bucket, bucketKeySpec) if err != nil { return api.Sandbox{}, err } + // Avoid assigning a typed-nil (*anypb.Any)(nil) to the typeurl.Any interface, + // which would produce a non-nil interface value that fails type-URL lookup. + if spec != nil { + inst.Spec = spec + } runtimeBucket := bucket.Bucket(bucketKeyRuntime) if runtimeBucket == nil { @@ -382,10 +387,13 @@ func (s *sandboxStore) read(parent *bbolt.Bucket, id []byte) (api.Sandbox, error } inst.Runtime.Name = string(runtimeBucket.Get(bucketKeyName)) - inst.Runtime.Options, err = boltutil.ReadAny(runtimeBucket, bucketKeyOptions) + runtimeOpts, err := boltutil.ReadAny(runtimeBucket, bucketKeyOptions) if err != nil { return api.Sandbox{}, err } + if runtimeOpts != nil { + inst.Runtime.Options = runtimeOpts + } inst.Extensions, err = boltutil.ReadExtensions(bucket) if err != nil { diff --git a/core/metadata/sandbox_test.go b/core/metadata/sandbox_test.go index 94cd428bd3..93bb7d5bc4 100644 --- a/core/metadata/sandbox_test.go +++ b/core/metadata/sandbox_test.go @@ -264,3 +264,40 @@ func assertEqualInstances(t *testing.T, x, y api.Sandbox) { t.Fatalf("x and y are different: %s", diff) } } + +// TestSandboxNilInterfaceRoundTrip verifies that a sandbox stored without a +// Spec or Runtime.Options round-trips back with genuinely nil interface values. +// +// compareNil (used by assertEqualInstances) treats a typed-nil pointer wrapped +// in an interface as equal to a true nil interface, so it would not catch a +// regression where ReadAny returns (*types.Any)(nil) and that typed-nil is +// stored directly in the typeurl.Any field — producing a non-nil interface +// that fails type-URL lookup at runtime. +func TestSandboxNilInterfaceRoundTrip(t *testing.T) { + ctx, db := testDB(t) + store := NewSandboxStore(db) + + in := api.Sandbox{ + ID: "nil-fields", + Runtime: api.RuntimeOpts{Name: "test"}, + // Spec and Runtime.Options are intentionally left nil. + } + + if _, err := store.Create(ctx, in); err != nil { + t.Fatal(err) + } + + out, err := store.Get(ctx, "nil-fields") + if err != nil { + t.Fatal(err) + } + + // Use direct interface comparison — not compareNil — to distinguish a + // true nil interface from a typed-nil pointer wrapped in an interface. + if out.Spec != nil { + t.Errorf("Spec: want nil interface, got %T(%v)", out.Spec, out.Spec) + } + if out.Runtime.Options != nil { + t.Errorf("Runtime.Options: want nil interface, got %T(%v)", out.Runtime.Options, out.Runtime.Options) + } +}