Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce initial wasm support #2818

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Conversation

mauriciovasquezbernal
Copy link
Member

@mauriciovasquezbernal mauriciovasquezbernal commented May 7, 2024

This PR introduces wasm support to handle events in user space.

With this support, gadgets can implement wasm modules that are able to:

  • Add new data sources
  • Add new fields to existing datasources
  • Change the value of fields

Based on initial POC by @flyth in https://github.com/inspektor-gadget/inspektor-gadget/tree/michael/oci/wasm.

TODO

Issues to open after merging

Testing

Use the trace_exec gadget modified on this PR or follow the hello-world-gadget-wasm guide to learn more about this.

Fixes #2202

Copy link
Member

@flyth flyth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a first pass, looking good so far. Will take another look later on.

I really like the "hello world" folllow-up!

gadgets/trace_dns/program.go Outdated Show resolved Hide resolved
docs/devel/hello-world-gadget-wasm.md Outdated Show resolved Hide resolved
docs/devel/hello-world-gadget-wasm.md Show resolved Hide resolved
docs/reference/wasm.md Outdated Show resolved Hide resolved
docs/reference/wasm.md Outdated Show resolved Hide resolved
pkg/operators/wasm/fields.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Show resolved Hide resolved
@mauriciovasquezbernal
Copy link
Member Author

I think it's ready for review.

  • I added some tests (they depend on API: Support running gadgets from files and memory #2853 as they're run from a file)
  • I'd love to get feedback on the "image/build: Use correct Inspektor Gadget code in in-tree gadgets" commit. I tried different approaches and none convinced me 100%.

Copy link
Member

@blanquicet blanquicet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the introduction of the wasm operator in the first commit. I have a couple of comments but the code is already pretty well structured and clear. It was easy to review it. Thanks!

pkg/operators/wasm/wasm.go Show resolved Hide resolved
Comment on lines +81 to +99
err := i.init(gadgetCtx)
if err != nil {
return fmt.Errorf("initializing wasm: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why operator initialization isn't done in the InstantiateImageOperator phase as for ebpf operator?

IMO, it makes no sense to succeed in the instantiation phase but then fail in the prepare phase because we wasn't able to read the program from the image or because the wasm module doesn't export the malloc function. I see the Prepare more like a phase where we prepare complementary parts that the operator will use while running. For instance, for the ebpf operator, we subscribe the converters, create the tchandler, network-tracer and uprobe-tracer, but everything regarding the ebpf operator itself was already initialized before, so we already know the program is valid and can be run.

Additionally, It'll prevent us from carrying the oras targer and image descriptor in wasmOperatorInstance, but just the WASM module already instantiated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not totally clear to me what are different phases for. Let's ask @flyth about this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow thought I had already answered to this, sorry.

This has a short overview of the lifecycle hooks and what should be done when. It's crucial that this operator forwards the init in the instantiate phase, as that is the only phase that will be called for GetGadgetInfo(). It is the phase in which all new data sources and alterations to existing ones need to be done in order for all operators to pick them up. The Prepare() phase is an optional phase that you can use to subscribe to datasources - it should be forwarded to Wasm as well.

i.mod = mod

// We need to call malloc on the guest to pass strings
i.guestMalloc = mod.ExportedFunction("malloc")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Perhaps, it makes more sense to add this function in wasm: Expose field functions to guest commit, as you did for dsCallback.

i.mod = mod

// We need to call malloc on the guest to pass strings
i.guestMalloc = mod.ExportedFunction("malloc")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we should check also here that the module exports init, start and stop functions and don't even instantiate the wasm operator if it doesn't. This reinforce my suggestion to move the init function into InstantiateImageOperator.

pkg/operators/wasm/wasm.go Show resolved Hide resolved
Copy link
Member

@blanquicet blanquicet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments from "wasm: Expose gadgetLog function to guest" commit.

pkg/operators/wasm/wasm.go Show resolved Hide resolved
pkg/operators/wasm/helpers.go Outdated Show resolved Hide resolved
pkg/operators/wasm/log.go Show resolved Hide resolved
pkg/operators/wasm/log.go Outdated Show resolved Hide resolved
Comment on lines 32 to 48
switch stack[0] {
case 0:
i.logger.Error(buf)
case 1:
i.logger.Warn(buf)
case 2:
i.logger.Info(buf)
case 3:
i.logger.Debug(buf)
case 4:
i.logger.Trace(buf)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be conceptually wrong to import github.com/inspektor-gadget/inspektor-gadget/pkg/operators/wasm/api as igwapi here and do this to keep them synchronized?

Suggested change
switch stack[0] {
case 0:
i.logger.Error(buf)
case 1:
i.logger.Warn(buf)
case 2:
i.logger.Info(buf)
case 3:
i.logger.Debug(buf)
case 4:
i.logger.Trace(buf)
}
switch logLevel {
case uint32(igwapi.ErrorLevel):
i.logger.Error(buf)
case uint32(igwapi.WarnLevel):
i.logger.Warn(buf)
case uint32(igwapi.InfoLevel):
i.logger.Info(buf)
case uint32(igwapi.DebugLevel):
i.logger.Debug(buf)
case uint32(igwapi.TraceLevel):
i.logger.Trace(buf)
}

pkg/operators/wasm/api/helpers.go Outdated Show resolved Hide resolved
pkg/operators/wasm/log.go Show resolved Hide resolved

var errInvalidPtr = errors.New("invalid pointer")

func bufFromStack(m wapi.Module, val uint64) ([]byte, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if it'd make sense also here to create a new type bufPtr here that implements String() and Bytes(), but maybe it's not worth it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean something like type bufPtr uint64 and implement those two methods on this? (as done in pkg/operators/wasm/api/helpers.go). I don't see a clear advantage in this case.

Copy link
Member

@blanquicet blanquicet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments about the datasource functions. Many of them are more questions than comments 🙂

pkg/operators/wasm/datasource.go Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
pkg/operators/wasm/wasm.go Outdated Show resolved Hide resolved
func (i *wasmOperatorInstance) newDataSource(ctx context.Context, m wapi.Module, stack []uint64) {
dsName, err := stringFromStack(m, stack[0])
if err != nil {
i.logger.Warnf("reading string from stack: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too generic and the same log is printed in a couple of situation in this same package. It's a log not and returned error, so IMO it should contain a little bit more context. Don't you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the name of the function to the log message. Did you have something else in mind?

pkg/operators/wasm/datasource.go Outdated Show resolved Hide resolved
Comment on lines +230 to +258
type subscriptionType uint32

const (
subscriptionTypeData subscriptionType = 0
subscriptionTypeArray subscriptionType = 1
subscriptionTypePacket subscriptionType = 2
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we expose these so that both API and operator can use the same consts?

pkg/operators/wasm/datasource.go Show resolved Hide resolved
pkg/operators/wasm/datasource.go Outdated Show resolved Hide resolved
Copy link
Member

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!

I will take a deeper look later, but here are some initial comments.

Best regards.

pkg/operators/wasm/wasm.go Show resolved Hide resolved
pkg/operators/wasm/wasm.go Show resolved Hide resolved
pkg/operators/wasm/wasm.go Show resolved Hide resolved
Comment on lines 114 to 206
reader, err := oci.GetContentFromDescriptor(gadgetCtx.Context(), i.desc)
if err != nil {
return fmt.Errorf("getting wasm program: %w", err)
}

wasmProgram, err := io.ReadAll(reader)
if err != nil {
reader.Close()
return fmt.Errorf("reading wasm program: %w", err)
}
reader.Close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a déjà-vu for this code.
Should we create a common function shared by both eBPF and WASM layers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do it., but TBH I don't think reducing 10 lines of duplicated code makes a big change. Would you mind opening an issue if you care about it more?

pkg/operators/wasm/wasm.go Show resolved Hide resolved
pkg/operators/wasm/api/datasource.go Outdated Show resolved Hide resolved
pkg/operators/wasm/datasource.go Outdated Show resolved Hide resolved
pkg/operators/wasm/datasource.go Show resolved Hide resolved
Comment on lines 120 to 143
handleIndex := i.lastHandleIndex
handleIndex++

// look for a free index in the map
for {
// zero is reserved
if handleIndex == 0 {
handleIndex++
}

if _, ok := i.handleMap[handleIndex]; !ok {
// register new entry
i.handleMap[handleIndex] = obj
i.lastHandleIndex = handleIndex
return handleIndex
}
handleIndex++
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand this code.
Despite having a map, you are looping on index, like an array, so you are doing O(n).
Instead of using this index, cannot you generate some random number?
Or using some hash as key, this way you guarantee uniqueness?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite having a map, you are looping on index, like an array, so you are doing O(n)

I think it's only O(n) in the worst case when the map is almost full and it has to check all possible combinations before finding the only available slot. In the first 2^32 calls to addHandle() the algorithm will return in O(1).

Instead of using this index, cannot you generate some random number?

I thought about this idea but I found some issues:

  • The returned handled will be random, I think it's easier to understand sequential handles (like file descriptors in linux).
  • If there's a collision we'll have to retry. If the map has few available slots, it'll require a lot of retries. Even if I think it shouldn't be a real issue given that wasm modules should use only few handles.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite having a map, you are looping on index, like an array, so you are doing O(n)

I think it's only O(n) in the worst case when the map is almost full and it has to check all possible combinations before finding the only available slot. In the first 2^32 calls to addHandle() the algorithm will return in O(1).

O(something) is always about the worst case.

Instead of using this index, cannot you generate some random number?

I thought about this idea but I found some issues:

* The returned handled will be random, I think it's easier to understand sequential handles (like file descriptors in linux).

Good idea, how is this implemented in Linux? I suppose we can take some inspiration from there.

* If there's a collision we'll have to retry. If the map has few available slots, it'll require a lot of retries. Even if I think it shouldn't be a real issue given that wasm modules should use only few handles.

Instead of random number, you can compute a hash and you would not have to deal with this issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I think the current implementation works fine and we don't need to complicate it anymore. I looked at the implementation on Linux and AFAIU it's more complex, they for sure need to handle scalability problems we don't have.

Something I can make, is to limit the maximum number of opened handles a gadget can have, something like 4k should be more than enough.

pkg/operators/wasm/wasm.go Show resolved Hide resolved
The wasm operator handles wasm programs present on the gadget.
This commit introduces the operator, next commits will implement
functions that are exposed to the wasm guests.

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Signed-off-by: Michael Friese <mfriese@microsoft.com>
Copy link
Member

@alban alban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR. Some comments for the communication between the wasm host and guest.

pkg/operators/wasm/helpers.go Show resolved Hide resolved

// bufPtr encodes the pointer and length of a buffer as a uint64
// The pointer is stored in the lower 32 bits and the length in the upper 32 bits
type bufPtr uint64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it makes it easier to cast bufPtr into uint64 to be able to pass it as argument of the //go:wasmimport functions:

gadgetLog(uint32(level), uint64(stringToBufPtr(message)))

pkg/operators/wasm/api/log.go Outdated Show resolved Hide resolved
pkg/operators/wasm/fields.go Show resolved Hide resolved
pkg/operators/wasm/fields.go Show resolved Hide resolved
// See the License for the specific language governing permissions and
// limitations under the License.

package api
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add package documentation in pkg/operators/wasm/api/doc.go explaining that this package is meant to be used by Wasm modules and not by the core IG code nor external applications using IG Go packages.

I think it should be at a different pkg/ directory to emphasize this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added doc.go and moved it to pkg/apis/wasm, any thoughts on that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds fine to me as long as the doc.go explains what it is for. Did you forget to run git-add?

If we ever implement the same library in Rust, where would it go?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget to run git-add?

I just pushed.

If we ever implement the same library in Rust, where would it go?

I have no idea. Perhaps this api should be outside pkg and go to a new folder on the root? So we can have a single folder with all implementations.

gadgets-api? wasm-api? ..

  • go
  • rust
  • ...

- `kind` (u32): Kind of access: How to read the field.

Return value:
- Value of the field.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add explanation:

  • If the returned value is of type String or Bytes, it will be allocated inside the wasm guest memory by calling the function malloc. The Wasm module must either provide its own implementation of malloc or be compiled against a library which provides it such as libc. It is the responsibility of the caller to free the allocation.
  • The reference Wasm guest Go library "github.com/inspektor-gadget/inspektor-gadget/pkg/operators/wasm/api" automatically frees the memory as appropriate so if your Wasm module uses that reference implementation, you don't have to call free.
  • The function returns 0 in case of errors (ambiguous with scalar types like u32).

I would feel better with a prototype such as:

fieldAccessorGet(u32 acc, u32 data, u32 kind, u32 *error, u32 *buf, u32 buf_len)

Then:

  • the caller can know the error
    • no ambiguity: is it an error or is it u32(0)?
  • the caller is responsible for allocating the buffer (with malloc or something else).
    • IG does not have call malloc
    • the wasm module does not have to use cgo and export malloc
  • IG just has to check that buf_len is big enough to copy what it wants to copy.

pkg/operators/wasm/api/fields.go Outdated Show resolved Hide resolved
pkg/operators/wasm/api/fields.go Outdated Show resolved Hide resolved
Comment on lines +49 to +51
env.NewFunctionBuilder().
WithGoModuleFunction(wapi.GoModuleFunc(fn), params, results).
Export(name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how to enable future API changes. If we want to change fieldAccessorSet, will we define fieldAccessorSetV2, fieldAccessorSetV3 etc.?

@mauriciovasquezbernal mauriciovasquezbernal force-pushed the mauricio/wasm branch 2 times, most recently from abf87d1 to 011dab9 Compare May 27, 2024 16:55
Add function used to emit log messages to the gadget logger
instance.

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Signed-off-by: Michael Friese <mfriese@microsoft.com>
This commit also introduces the handle logic: A way to expose
host objects to the guest by using handles.

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Signed-off-by: Michael Friese <mfriese@microsoft.com>
Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Signed-off-by: Michael Friese <mfriese@microsoft.com>
Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Building in-tree gadgets is a bit more complicated as we want
them to use the current files to be sure they take any
changes we're working on. (It's specially relevant
for the api exposed to gadgets in pkg/operators/wasm/api/)

One big challenge is that (ideally) the build process should continue
to work when using a container or when building locally on the host (--local).

This commit achieves it by using a replace directive with a relative
path to the inspektor-gadget folder. Then the build command
expects a new --inspektor-gadget-path argument to mount the inspektor-gadget
path in the container.

This commit isn't totally clean, however other solutions seemed even more complicated:
- Copy the inspektor gadget source code to the builder image
 - pros:
  - We don't need to mount anything on the container
 - cons:
  - The ebpf builder image will be implicitly associated with an inspektor gadget version
- Always use --local for building in-tree gadgets
 - pros:
  - We don't need to worry about this issue at all
 - cons:
  - Complicates the CI as we'll need to install all dependencies locally to compile gadgets

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
The args field is encoded by the eBPF program as:
arg0\0arg1\0arg2, this commit implements the logic
to decode and convert it to a string using wasm.

TODO: The datasource doesn't support arrays yet, hence we have to
join the args in a single string. This could be wrong as it's
possible to execute a process with arguments that contain spaces.

TODO2: Are the args size and args count fields really needed? Can't we
imply them?, like two consecutive null bytes means the string ends, etc.

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
This commit updates few interfaces and functions signatures
to allow to run gadgets from a file or from memory when using
the Golang API.

Specifically:
- Update the gadget context to allow passing an oras target.
- Update GetManifestForHost() and GetContentFromDescriptor() to take
  the target instead of always using the local one store.

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
Add some tests that rely on gadgets stored as tar images

TODO:
- how to keep these .tar files updated?
- Why the tar files change each time make is executed? (timestamp?)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RFE] handling events in userspace with wasm
5 participants