diff --git a/docs/gadget-devel/gadget-wasm-api-raw.md b/docs/gadget-devel/gadget-wasm-api-raw.md index a988cea1f6..fa9ab073b5 100644 --- a/docs/gadget-devel/gadget-wasm-api-raw.md +++ b/docs/gadget-devel/gadget-wasm-api-raw.md @@ -280,3 +280,33 @@ Parameters: Return value: - None + +### Parameters + +Parameters passed to the WASM module are defined in the metadata file as this: + +```yaml +... +params: + wasm: + param-key: + key: param-key + description: param-description + defaultValue: param-default-value + typeHint: param-type-hint + title: param-title + alias: param-alias + isMandatory: true + param-key2: + ... +``` + +#### `getParamValue(key string) string` + +Return the value of a parameter. + +Parameters: +- `key` (string): Key of the parameter. + +Return value: +- The value of the parameter. diff --git a/pkg/operators/wasm/fields.go b/pkg/operators/wasm/fields.go index 0154e7185d..0ae4980d9b 100644 --- a/pkg/operators/wasm/fields.go +++ b/pkg/operators/wasm/fields.go @@ -72,21 +72,13 @@ func (i *wasmOperatorInstance) fieldGet(ctx context.Context, m wapi.Module, stac } handleBytes := func(buf []byte) uint64 { - res, err := i.guestMalloc.Call(ctx, uint64(len(buf))) + val, err := i.writeToGuestMemory(ctx, buf) if err != nil { - i.logger.Warnf("malloc failed: %v", err) - stack[0] = 0 - return 0 - - } - - if !m.Memory().Write(uint32(res[0]), buf) { - i.logger.Warnf("out of memory write") - stack[0] = 0 + i.logger.Warnf("fieldGet: writing bytes to guest memory: %v", err) return 0 } - return uint64(len(buf))<<32 | uint64(res[0]) + return val } var val uint64 diff --git a/pkg/operators/wasm/helpers.go b/pkg/operators/wasm/helpers.go index 5ed0bc57b6..488943750b 100644 --- a/pkg/operators/wasm/helpers.go +++ b/pkg/operators/wasm/helpers.go @@ -17,6 +17,7 @@ package wasm import ( "context" "errors" + "fmt" "github.com/tetratelabs/wazero" wapi "github.com/tetratelabs/wazero/api" @@ -33,6 +34,11 @@ func bufFromStack(m wapi.Module, val uint64) ([]byte, error) { } func stringFromStack(m wapi.Module, val uint64) (string, error) { + // handle empty strings in a special way + if val == 0 { + return "", nil + } + buf, err := bufFromStack(m, val) if err != nil { return "", err @@ -50,3 +56,16 @@ func exportFunction( WithGoModuleFunction(wapi.GoModuleFunc(fn), params, results). Export(name) } + +func (i *wasmOperatorInstance) writeToGuestMemory(ctx context.Context, buf []byte) (uint64, error) { + res, err := i.guestMalloc.Call(ctx, uint64(len(buf))) + if err != nil { + return 0, fmt.Errorf("malloc failed: %w", err) + } + + if !i.mod.Memory().Write(uint32(res[0]), buf) { + return 0, fmt.Errorf("out of memory write") + } + + return uint64(len(buf))<<32 | uint64(res[0]), nil +} diff --git a/pkg/operators/wasm/params.go b/pkg/operators/wasm/params.go new file mode 100644 index 0000000000..f409276b98 --- /dev/null +++ b/pkg/operators/wasm/params.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Inspektor Gadget authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wasm + +import ( + "context" + + "github.com/tetratelabs/wazero" + wapi "github.com/tetratelabs/wazero/api" +) + +func (i *wasmOperatorInstance) addParamsFuncs(env wazero.HostModuleBuilder) { + exportFunction(env, "getParamValue", i.getParamValue, + []wapi.ValueType{ + wapi.ValueTypeI64, // ParamKey + }, + []wapi.ValueType{wapi.ValueTypeI64}, // Value + ) +} + +// getParamValue returns the value of a param. +// Params: +// - stack[0] parameter key +// Return value: +// - Uint64 with the param's value, 0 on error +func (i *wasmOperatorInstance) getParamValue(ctx context.Context, m wapi.Module, stack []uint64) { + paramKeyPtr := stack[0] + + paramKey, err := stringFromStack(m, paramKeyPtr) + if err != nil { + i.logger.Warnf("getParamValue: reading string from stack: %v", err) + stack[0] = 0 + return + } + + val, ok := i.paramValues[paramKey] + if !ok { + i.logger.Warnf("getParamValue: param %q not found", paramKey) + stack[0] = 0 + return + } + + buf := []byte(val) + ret, err := i.writeToGuestMemory(ctx, buf) + if err != nil { + i.logger.Warnf("getParamValue: writing to guest memory: %v", err) + stack[0] = 0 + return + } + + stack[0] = ret +} diff --git a/pkg/operators/wasm/testdata/Makefile b/pkg/operators/wasm/testdata/Makefile index 7fdc68dede..48f2a42ee1 100644 --- a/pkg/operators/wasm/testdata/Makefile +++ b/pkg/operators/wasm/testdata/Makefile @@ -4,6 +4,7 @@ TEST_ARTIFACTS = \ fields \ dataarray \ badguest \ + params \ # all: $(TEST_ARTIFACTS) diff --git a/pkg/operators/wasm/testdata/badguest.tar b/pkg/operators/wasm/testdata/badguest.tar index 0fefc812be..37bf638f14 100644 Binary files a/pkg/operators/wasm/testdata/badguest.tar and b/pkg/operators/wasm/testdata/badguest.tar differ diff --git a/pkg/operators/wasm/testdata/badguest/go.mod b/pkg/operators/wasm/testdata/badguest/go.mod index f40acc37d1..b7ec2c67c5 100644 --- a/pkg/operators/wasm/testdata/badguest/go.mod +++ b/pkg/operators/wasm/testdata/badguest/go.mod @@ -1,6 +1,6 @@ module main -go 1.22.0 +go 1.22.7 require github.com/inspektor-gadget/inspektor-gadget v0.27.0 diff --git a/pkg/operators/wasm/testdata/badguest/program.go b/pkg/operators/wasm/testdata/badguest/program.go index 3ba8346152..1d9401cc7e 100644 --- a/pkg/operators/wasm/testdata/badguest/program.go +++ b/pkg/operators/wasm/testdata/badguest/program.go @@ -89,6 +89,9 @@ func fieldGet(acc uint32, data uint32, kind uint32) uint64 //go:wasmimport env fieldSet func fieldSet(acc uint32, data uint32, kind uint32, value uint64) uint32 +//go:wasmimport env getParamValue +func getParamValue(key uint64) uint64 + func stringToBufPtr(s string) uint64 { unsafePtr := unsafe.Pointer(unsafe.StringData(s)) return uint64(len(s))<<32 | uint64(uintptr(unsafePtr)) @@ -99,19 +102,19 @@ func logAndPanic(msg string) { panic(msg) } -func assertZero(v uint32, msg string) { +func assertZero[T uint64 | uint32](v T, msg string) { if v != 0 { logAndPanic(fmt.Sprintf("%d is not zero: %s", v, msg)) } } -func assertNonZero(v uint32, msg string) { +func assertNonZero[T uint64 | uint32](v T, msg string) { if v == 0 { logAndPanic(fmt.Sprintf("v is zero: %s", msg)) } } -func assertEqual(v1, v2 uint32, msg string) { +func assertEqual[T uint64 | uint32](v1, v2 T, msg string) { if v1 != v2 { logAndPanic(fmt.Sprintf("%d != %d: %s", v1, v2, msg)) } @@ -240,6 +243,10 @@ func gadgetInit() int { fieldGet(fieldHandle, fieldHandle, uint32(api.Kind_Uint32)) fieldGet(dataHandle, dataHandle, uint32(api.Kind_Uint32)) + /* Params */ + assertZero(getParamValue(stringToBufPtr("non-existing-param")), "getParamValue: not-found") + assertZero(getParamValue(invalidStrPtr), "getParamValue: invalid key ptr") + return 0 } diff --git a/pkg/operators/wasm/testdata/params.tar b/pkg/operators/wasm/testdata/params.tar new file mode 100644 index 0000000000..ece0fb45c7 Binary files /dev/null and b/pkg/operators/wasm/testdata/params.tar differ diff --git a/pkg/operators/wasm/testdata/params/build.yaml b/pkg/operators/wasm/testdata/params/build.yaml new file mode 100644 index 0000000000..9a6d0a5f80 --- /dev/null +++ b/pkg/operators/wasm/testdata/params/build.yaml @@ -0,0 +1 @@ +wasm: program.go diff --git a/pkg/operators/wasm/testdata/params/gadget.yaml b/pkg/operators/wasm/testdata/params/gadget.yaml new file mode 100644 index 0000000000..77cede446b --- /dev/null +++ b/pkg/operators/wasm/testdata/params/gadget.yaml @@ -0,0 +1,11 @@ +name: params_test +params: + wasm: + param-key: + key: param-key + description: param-description + defaultValue: param-default-value + typeHint: param-type-hint + title: param-title + alias: param-alias + isMandatory: true diff --git a/pkg/operators/wasm/testdata/params/go.mod b/pkg/operators/wasm/testdata/params/go.mod new file mode 100644 index 0000000000..b7ec2c67c5 --- /dev/null +++ b/pkg/operators/wasm/testdata/params/go.mod @@ -0,0 +1,8 @@ +module main + +go 1.22.7 + +require github.com/inspektor-gadget/inspektor-gadget v0.27.0 + +// use this to be able to compile it locally +replace github.com/inspektor-gadget/inspektor-gadget => ../../../../../ diff --git a/pkg/operators/wasm/testdata/params/program.bpf.c b/pkg/operators/wasm/testdata/params/program.bpf.c new file mode 100644 index 0000000000..5672b3f33d --- /dev/null +++ b/pkg/operators/wasm/testdata/params/program.bpf.c @@ -0,0 +1 @@ +// TODO: a c file is always needed by the gadget to be built diff --git a/pkg/operators/wasm/testdata/params/program.go b/pkg/operators/wasm/testdata/params/program.go new file mode 100644 index 0000000000..52fb02a590 --- /dev/null +++ b/pkg/operators/wasm/testdata/params/program.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Inspektor Gadget authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This program tries as hard as it can to break the host by calling functions +// with wrong arguments. It uses the low level functions directly as the goal is +// to test the host and not the wrapper API. Tests under dataarray and fields +// test also the higher level API. +package main + +import ( + api "github.com/inspektor-gadget/inspektor-gadget/wasmapi/go" +) + +//export gadgetStart +func gadgetStart() int { + val, err := api.GetParamValue("param-key") + if err != nil { + api.Errorf("failed to get param: %v", err) + return 1 + } + + const expected = "param-value" + if val != expected { + api.Errorf("param value should be %q, got: %q", expected, val) + return 1 + } + + _, err = api.GetParamValue("non-existing-param") + if err == nil { + api.Errorf("looking for non-existing-param succeded") + return 1 + } + + return 0 +} + +func main() {} diff --git a/pkg/operators/wasm/wasm.go b/pkg/operators/wasm/wasm.go index 0c4e4f4e0e..109543100d 100644 --- a/pkg/operators/wasm/wasm.go +++ b/pkg/operators/wasm/wasm.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/go-multierror" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/viper" "github.com/tetratelabs/wazero" wapi "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" @@ -65,9 +66,10 @@ func (w *wasmOperator) InstantiateImageOperator( operators.ImageOperatorInstance, error, ) { instance := &wasmOperatorInstance{ - gadgetCtx: gadgetCtx, - handleMap: map[uint32]any{}, - logger: gadgetCtx.Logger(), + gadgetCtx: gadgetCtx, + handleMap: map[uint32]any{}, + logger: gadgetCtx.Logger(), + paramValues: paramValues, } if err := instance.init(gadgetCtx, target, desc); err != nil { @@ -75,6 +77,23 @@ func (w *wasmOperator) InstantiateImageOperator( return nil, fmt.Errorf("initializing wasm: %w", err) } + var config *viper.Viper + if configVar, ok := gadgetCtx.GetVar("config"); ok { + config, _ = configVar.(*viper.Viper) + } + + if config != nil { + extraParams := map[string]*api.Param{} + err := config.UnmarshalKey("params.wasm", &extraParams) + if err != nil { + return nil, fmt.Errorf("unmarshalling extra params: %w", err) + } + + for _, v := range extraParams { + instance.extraParams = append(instance.extraParams, v) + } + } + return instance, nil } @@ -94,6 +113,9 @@ type wasmOperatorInstance struct { handleMap map[uint32]any lastHandleIndex uint32 handleLock sync.RWMutex + + extraParams api.Params + paramValues map[string]string } func (i *wasmOperatorInstance) Name() string { @@ -109,7 +131,7 @@ func (i *wasmOperatorInstance) Prepare(gadgetCtx operators.GadgetContext) error } func (i *wasmOperatorInstance) ExtraParams(gadgetCtx operators.GadgetContext) api.Params { - return nil + return i.extraParams } func (i *wasmOperatorInstance) addHandle(obj any) uint32 { @@ -190,6 +212,7 @@ func (i *wasmOperatorInstance) init( i.addLogFuncs(env) i.addDataSourceFuncs(env) i.addFieldFuncs(env) + i.addParamsFuncs(env) if _, err := env.Instantiate(ctx); err != nil { return fmt.Errorf("instantiating host module: %w", err) diff --git a/pkg/operators/wasm/wasm_test.go b/pkg/operators/wasm/wasm_test.go index 1de5e5aaeb..c44c647246 100644 --- a/pkg/operators/wasm/wasm_test.go +++ b/pkg/operators/wasm/wasm_test.go @@ -326,3 +326,57 @@ func TestBadGuest(t *testing.T) { err = runtime.RunGadget(gadgetCtx, nil, params) require.NoError(t, err, "running gadget") } + +func TestWasmParams(t *testing.T) { + utilstest.RequireRoot(t) + + t.Parallel() + + myOperator := simple.New("myHandler", + simple.OnStart(func(gadgetCtx operators.GadgetContext) error { + params := gadgetCtx.Params() + found := false + for _, p := range params { + if p.Key == "param-key" { + require.Equal(t, "param-description", p.Description) + require.Equal(t, "param-default-value", p.DefaultValue) + require.Equal(t, "param-type-hint", p.TypeHint) + require.Equal(t, "param-title", p.Title) + require.Equal(t, "param-alias", p.Alias) + require.True(t, p.IsMandatory) + + found = true + break + } + } + + require.True(t, found, "param not found") + return nil + }), + ) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + t.Cleanup(cancel) + + ociStore, err := orasoci.NewFromTar(ctx, "testdata/params.tar") + require.NoError(t, err, "creating oci store") + + gadgetCtx := gadgetcontext.New( + ctx, + "params:latest", + gadgetcontext.WithDataOperators(ocihandler.OciHandler, myOperator), + gadgetcontext.WithOrasReadonlyTarget(ociStore), + ) + + runtime := local.New() + err = runtime.Init(nil) + require.NoError(t, err, "runtime init") + t.Cleanup(func() { runtime.Close() }) + + params := map[string]string{ + "operator.oci.verify-image": "false", + "operator.oci.wasm.param-key": "param-value", + } + err = runtime.RunGadget(gadgetCtx, nil, params) + require.NoError(t, err, "running gadget") +} diff --git a/wasmapi/go/params.go b/wasmapi/go/params.go new file mode 100644 index 0000000000..92bb66d563 --- /dev/null +++ b/wasmapi/go/params.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Inspektor Gadget authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" +) + +//go:wasmimport env getParamValue +func getParamValue(key uint64) uint64 + +func GetParamValue(key string) (string, error) { + k := uint64(stringToBufPtr(key)) + val := bufPtr(getParamValue(k)) + if val == 0 { + return "", errors.New("error getting param value") + } + + ret := val.string() + val.free() + return ret, nil +}