From ad0040261d10285e605de92efefec7f525ee7d3d Mon Sep 17 00:00:00 2001 From: zanarelli Date: Fri, 31 Jul 2026 23:19:54 -0300 Subject: [PATCH] fix(postrenderer): don't panic on a post-renderer plugin's malformed output postRendererPlugin.Run type-asserted output.Message with the unchecked, single-value form (output.Message.(schema.OutputMessagePostRendererV1)), unlike the equivalent, already-guarded assertion in getterPlugin.Get (pkg/getter/plugingetter.go), and then called outputMessage.Manifests.Bytes() without checking whether Manifests was nil. A plugin whose Invoke() returns a message with a nil Manifests field (which is exactly what json.Unmarshal produces for a WASM/extism/v1 plugin's output when its JSON response omits the "manifests" key or sets it to null) crashes the whole helm process with an unrecovered nil-pointer-dereference panic instead of a clean error. This is a distinct, unrecovered-panic edge case of the already-known, still-open "WASM post-renderer plugins receive empty manifests" defect (#31832, attempted fix in stale/closed #31834): that report's own echo-plugin reproducer round-trips {"manifests":{}} rather than {"manifests":null}/omitted, which unmarshals to a non-nil empty Buffer and therefore only hits the existing graceful "produced empty output" error, not this panic. Add the same ok-checked type assertion getterPlugin.Get already uses, and treat a nil Manifests the same as an empty one. Signed-off-by: zanarelli --- pkg/postrenderer/postrenderer.go | 12 ++++++---- pkg/postrenderer/postrenderer_test.go | 32 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/pkg/postrenderer/postrenderer.go b/pkg/postrenderer/postrenderer.go index 55e6d3adf..5a51e00e4 100644 --- a/pkg/postrenderer/postrenderer.go +++ b/pkg/postrenderer/postrenderer.go @@ -72,11 +72,15 @@ func (r *postRendererPlugin) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer return nil, fmt.Errorf("failed to invoke post-renderer plugin %q: %w", r.plugin.Metadata().Name, err) } - outputMessage := output.Message.(schema.OutputMessagePostRendererV1) + outputMessage, ok := output.Message.(schema.OutputMessagePostRendererV1) + if !ok { + return nil, fmt.Errorf("invalid output message type from plugin %q", r.plugin.Metadata().Name) + } - // If the binary returned almost nothing, it's likely that it didn't - // successfully render anything - if len(bytes.TrimSpace(outputMessage.Manifests.Bytes())) == 0 { + // If the binary returned almost nothing (including no Manifests buffer at + // all, which a malformed or misbehaving plugin can produce), it's likely + // that it didn't successfully render anything. + if outputMessage.Manifests == nil || len(bytes.TrimSpace(outputMessage.Manifests.Bytes())) == 0 { return nil, fmt.Errorf("post-renderer %q produced empty output", r.plugin.Metadata().Name) } diff --git a/pkg/postrenderer/postrenderer_test.go b/pkg/postrenderer/postrenderer_test.go index cea6f9f76..764194dd4 100644 --- a/pkg/postrenderer/postrenderer_test.go +++ b/pkg/postrenderer/postrenderer_test.go @@ -18,15 +18,47 @@ package postrenderer import ( "bytes" + "context" "runtime" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v4/internal/plugin" + "helm.sh/helm/v4/internal/plugin/schema" "helm.sh/helm/v4/pkg/cli" ) +// nilManifestPlugin simulates a plugin.Plugin whose Invoke() returns a +// well-typed OutputMessagePostRendererV1 with a nil Manifests field. This is +// exactly what internal/plugin's ExtismV1PluginRuntime.Invoke produces (via +// json.Unmarshal into a freshly reflect.New'd struct) when a wasm/extism +// post-renderer plugin's JSON output omits the "manifests" key or sets it to +// null -- e.g. because the plugin errored internally but still exited 0, or +// is simply buggy/malformed. No handcrafted wasm binary is required to prove +// the bug: the defect is in postRendererPlugin.Run's handling of the output +// message, not in how a specific Runtime constructs it. +type nilManifestPlugin struct{} + +func (nilManifestPlugin) Dir() string { return "" } +func (nilManifestPlugin) Metadata() plugin.Metadata { + return plugin.Metadata{Name: "nil-manifest-plugin", Type: "postrenderer/v1"} +} + +func (nilManifestPlugin) Invoke(_ context.Context, _ *plugin.Input) (*plugin.Output, error) { + return &plugin.Output{ + Message: schema.OutputMessagePostRendererV1{Manifests: nil}, + }, nil +} + +func TestPostRendererPluginRunWithNilManifestsDoesNotPanic(t *testing.T) { + r := &postRendererPlugin{plugin: nilManifestPlugin{}} + + _, err := r.Run(bytes.NewBufferString("apiVersion: v1\nkind: ConfigMap\n")) + require.Error(t, err, "a plugin returning nil Manifests should produce a clean error, not a panic") +} + func TestNewPostRenderPluginRunWithNoOutput(t *testing.T) { if runtime.GOOS == "windows" { // the actual Run test uses a basic sed example, so skip this test on windows