-
Notifications
You must be signed in to change notification settings - Fork 185
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
base: main
Are you sure you want to change the base?
Conversation
1f3dd29
to
d9d6927
Compare
There was a problem hiding this 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!
d9d6927
to
903bcb4
Compare
903bcb4
to
e88f56f
Compare
I think it's ready for review.
|
e88f56f
to
bc8d6b1
Compare
There was a problem hiding this 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!
err := i.init(gadgetCtx) | ||
if err != nil { | ||
return fmt.Errorf("initializing wasm: %w", err) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
.
There was a problem hiding this 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/log.go
Outdated
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) | ||
} |
There was a problem hiding this comment.
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?
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) | |
} |
|
||
var errInvalidPtr = errors.New("invalid pointer") | ||
|
||
func bufFromStack(m wapi.Module, val uint64) ([]byte, error) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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
Outdated
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
type subscriptionType uint32 | ||
|
||
const ( | ||
subscriptionTypeData subscriptionType = 0 | ||
subscriptionTypeArray subscriptionType = 1 | ||
subscriptionTypePacket subscriptionType = 2 | ||
) |
There was a problem hiding this comment.
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?
bc8d6b1
to
ee8f534
Compare
There was a problem hiding this 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
Outdated
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Outdated
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++ | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
ee8f534
to
dde4262
Compare
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>
There was a problem hiding this 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/api/helpers.go
Outdated
|
||
// 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 |
There was a problem hiding this comment.
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/datasource.go
Outdated
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package api |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
- ...
docs/reference/wasm.md
Outdated
- `kind` (u32): Kind of access: How to read the field. | ||
|
||
Return value: | ||
- Value of the field. |
There was a problem hiding this comment.
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)
?
- no ambiguity: is it an error or is it
- 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.
env.NewFunctionBuilder(). | ||
WithGoModuleFunction(wapi.GoModuleFunc(fn), params, results). | ||
Export(name) |
There was a problem hiding this comment.
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.?
abf87d1
to
011dab9
Compare
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?)
011dab9
to
5614bc1
Compare
This PR introduces wasm support to handle events in user space.
With this support, gadgets can implement wasm modules that are able to:
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