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 <zanarelli.dev@gmail.com>
pull/32502/head
zanarelli 1 month ago
parent a8ab76e86f
commit ad0040261d

@ -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)
}

@ -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

Loading…
Cancel
Save