diff --git a/pkg/workflows/wasm/host/internal/rawsdk/helpers_wasip1.go b/pkg/workflows/wasm/host/internal/rawsdk/helpers_wasip1.go index 3bbfa0a2ac..409acbc880 100644 --- a/pkg/workflows/wasm/host/internal/rawsdk/helpers_wasip1.go +++ b/pkg/workflows/wasm/host/internal/rawsdk/helpers_wasip1.go @@ -197,7 +197,8 @@ func await[I, O proto.Message](input I, output O, fn awaitFn) { bytes := fn(mptr, mlen, responsePtr, responseLen) if bytes < 0 { - SendError(errors.New("awaitCapabilities returned an error")) + response = response[:-bytes] + SendError(fmt.Errorf("awaitCapabilities returned an error %s", string(response))) } if proto.Unmarshal(response[:bytes], output) != nil { diff --git a/pkg/workflows/wasm/host/module.go b/pkg/workflows/wasm/host/module.go index 73aad9f098..493d209031 100644 --- a/pkg/workflows/wasm/host/module.go +++ b/pkg/workflows/wasm/host/module.go @@ -1066,7 +1066,9 @@ func truncateWasmWrite(caller *wasmtime.Caller, src []byte, ptr int32, size int3 src = src[:size] } - return write(memory, src, ptr, size) + // truncateWasmWrite is only called for returning error strings + // Therefore, we need to return the negated bytes written to indicate the failure to the guest. + return -write(memory, src, ptr, size) } // write copies the given src byte slice into the memory at the given pointer and max size. diff --git a/pkg/workflows/wasm/host/standard_test.go b/pkg/workflows/wasm/host/standard_test.go index 4273c35647..eb17367d92 100644 --- a/pkg/workflows/wasm/host/standard_test.go +++ b/pkg/workflows/wasm/host/standard_test.go @@ -118,6 +118,37 @@ func TestStandardCapabilityCallsAreAsync(t *testing.T) { assert.Equal(t, "truefalse", result) } +func TestStandardHostWasmWriteErrorsAreRespected(t *testing.T) { + t.Parallel() + mockExecutionHelper := NewMockExecutionHelper(t) + mockExecutionHelper.EXPECT().GetNodeTime().RunAndReturn(func() time.Time { + return time.Now() + }).Maybe() + mockExecutionHelper.EXPECT().GetWorkflowExecutionID().Return("id") + mockExecutionHelper.EXPECT().CallCapability(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, request *sdk.CapabilityRequest) (*sdk.CapabilityResponse, error) { + // In this test the response from the capability is successful, + // but the WASM didn't provide a large enough buffer to fit it + // 500 MB will suffice for the overflow on writes. + + tooLargeResponse := make([]byte, 500000000) + + // Since the bytes in the payload shouldn't be read, we don't need a valid proto + payload := &anypb.Any{ + TypeUrl: "fake", + Value: tooLargeResponse, + } + + return &sdk.CapabilityResponse{Response: &sdk.CapabilityResponse_Payload{Payload: payload}}, nil + }) + + m := makeTestModule(t) + request := triggerExecuteRequest(t, 0, &basictrigger.Outputs{CoolOutput: anyTestTriggerValue}) + errStr := executeWithError(t, m, request, mockExecutionHelper) + + // Use Contains instead of Equal for flexibility, as languages have different conventions for errors. + require.Contains(t, errStr, ResponseBufferTooSmall) +} + func TestStandardModeSwitch(t *testing.T) { t.Parallel() t.Run("successful mode switch", func(t *testing.T) { diff --git a/pkg/workflows/wasm/host/standard_tests/host_wasm_write_errors_are_respected/main_wasip1.go b/pkg/workflows/wasm/host/standard_tests/host_wasm_write_errors_are_respected/main_wasip1.go new file mode 100644 index 0000000000..3a7d3025e3 --- /dev/null +++ b/pkg/workflows/wasm/host/standard_tests/host_wasm_write_errors_are_respected/main_wasip1.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/protoc/pkg/test_capabilities/basicaction" + "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host/internal/rawsdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" +) + +func main() { + input := &basicaction.Inputs{InputThing: true} + rId := rawsdk.DoRequestAsync("basic-test-action@1.0.0", "PerformAction", sdk.Mode_MODE_DON, input) + + rawsdk.Await(rId, &basicaction.Outputs{}) + rawsdk.SendResponse("should not get here as Await sends error on errors...") +}