feat: added WASI runtime and js/go WASM packages#594
feat: added WASI runtime and js/go WASM packages#594niallnsec wants to merge 4 commits intoVirusTotal:mainfrom
Conversation
Signed-off-by: Niall Newman <nn@turacolabs.com>
The only point where `wasmtime` is mentioned is the `wasm/runtime.rs` file, which re-exports everything from `wasmtime` or re-implement it.
56311d7 to
b167236
Compare
Signed-off-by: Niall Newman <nn@turacolabs.com>
|
This is too much code for a single PR, and it contains changes that are not necesarily related like the |
|
Totally understand. Its probably quite easy to split up into 3 pieces off the bat. All of the changes under the lib directory are focused on the WASM runtime and don't really touch anything else. Its essentially a continuation of the previous change plus the extra support for building for a WASI target. Then js-wasm and go-wasm are pretty self contained so I can take them out entirely and submit them individual or make them distinct packages outside of this repo if you'd prefer. I believe go-wasm contains the largest share of code in this PR which is one of the reasons I was already concerned about its size and maintenance burden. |
For now I have this question: which advantages offers the After a brief investigation my conclusion is that I haven't spent enough time yet digging into the previous PR that added support for WASM, I would like to understand this better. |
|
The main advantage of the In both cases the generated rule WASM is executed without embedding Wasmtime inside
Paths that use Taking So generally speaking:
|
|
In order to try and make things a bit easier to review, I thought it might be helpful to explain in a bit more detail the design decisions that went into this PR and the previous one as well as the process I went through, as it was very much an iterative design. The first objective I had was to make it possible to run Once I understood that properly, the obvious approach was to remove the dependency on Wasmtime for Wasm targets and let the surrounding host runtime execute the generated rule Wasm directly. In the browser case, that means using the host I did not try to extend the browser-side host integration beyond what is needed to get the core functionality of Once I had the browser runtime working, it occurred to me that the same overall approach could be used to produce a set of Go bindings that do not require CGO. One of the first things I ran into there was that Because When a full WASI environment is available on the host, That said, the Go bindings still need their own host-guest ABI on top of WASI. This is an important distinction, because there are actually two different interfaces involved in this work. The first is the internal runtime interface used by The second is the public ABI used by A separate detail that came out of the performance work was synchronization of imported module state across the boundary between generated rule Wasm and the host runtime. Both the browser runtime and the That performance work was important to me from the start. I use YARA heavily from Go in day-to-day work, and while CGO works, it is awkward to maintain and awkward to deploy. A pure Go build is useful not just because it is cleaner in principle, but because it simplifies the build system substantially and makes it possible to use YARA in places where carrying a native C dependency is inconvenient or undesirable. My aim with A large part of that work led into the experimental allocator and the That work is also what led me to look into the feasibility of a Since both the browser runtime for I think this is a good structure because it leaves the vast majority of the library untouched and keeps the Wasm-specific support isolated to a clearly defined runtime abstraction layer. There are still some important limitations, and they differ between the browser and The browser target is the most constrained. There is no practical equivalent there to the native timeout and interruption mechanisms used in the non-Wasm build, so timeout configuration in the The Overall, the approach I ended up with was to keep the core of |
|
Thanks for your detailed explanation. I took some time to think carefully about this, and I came to these conclusions:
So, if you want to work on a PR that includes only the JS bindings that would be really great. |
|
Thanks for taking the time to review this. I have submitted a new PR with just the JS focused changes. If we can get that to a place where you are happy with it to be merged, then after I'll move just the wasip1 runtime build support into a followup PR without all the extensive go-wasm binding code if thats okay. |
This PR builds on the initial browser runtime support that is already present in YARA-X from my earlier PR. The main purpose of this change is to tidy up the WASM runtime code now that there are multiple backends, and to add two new pieces on top of that existing support:
js-wasmgo-wasmAlongside that, I have added support for a
wasm32-wasip1build and expanded the test coverage for both the browser and Go/WASM paths.Runtime Refactor
YARA-X now has three runtime backends for executing compiled rules:
wasmtimewasm32-unknown-unknownwasm32-wasip1As these pieces developed, the browser and WASI runtimes had started to duplicate too much of the same Wasmtime-shaped shim code. I have restructured that under
lib/src/wasm/runtimeso that:mod.rsis just the backend selection and re-export layernative.rshandles the nativewasmtimepathcommon.rscontains the shared compatibility layer used by the custom runtimesbrowser.rscontains browser-specific behaviourwasip1.rscontains WASI-specific behaviourThe aim was to separate shared machinery from backend-specific logic. That makes the layout easier to follow and should make future maintenance less awkward.
I also renamed the Cargo feature from
wasip2-runtimetowasip1-runtime, which better reflects the target actually being built.Browser JavaScript Package
One entirely new part of this PR is
js-wasm.Although YARA-X already had the underlying browser runtime support, it did not yet have a browser-facing JavaScript package that exposed that functionality in a usable form. This PR adds that package in-tree.
I also moved its standard packaging flow onto
wasm-pack, with the default generated package inpkg/and additional no-modules bundle output indist/.The JavaScript API is unchanged, but the build and test setup should now be much more familiar to anyone who has worked with Rust/WASM projects before.
Go/WASM Bindings
The other major addition in this PR is
go-wasm.I use YARA from Go heavily in my day-to-day work, pushing very large volumes of data through it every week. The existing CGO-based approach works, but CGO is awkward to maintain and deploy, and it complicates the build process far more than I would like. To borrow the usual phrase, "cgo is not Go". A pure Go build of YARA is extremely useful to me, and I think it is likely to be useful to others as well, because it makes the build system much simpler and allows it to run in many more places. (eg. github.com/Velocidex/yara-x-go)
That is the reason for the WASI guest build and the
go-wasmbindings. The aim here was not just to prove that it can be done, but to produce something that is actually practical to use from normal Go code.Why the experimental allocator exists
A large part of the
go-wasmwork was driven by scanning behaviour rather than just packaging.The existing block-scanning approach in YARA is very limited, to the point that it is not really usable for a lot of the cases I care about. What I wanted was a way to keep using the normal scanning flow while still being able to supply data lazily, without having to load the whole input into memory up front.
That is what led to the experimental allocator and the
userfaultfdwork. It allows the standard scan calls to operate against guest memory that is populated on demand at runtime. In practice, that makes it possible to handle Go-style scanning workloads much more naturally, especially where the source data is large or where loading everything eagerly would be undesirable.I think that is a much better fit for real Go code than relying on YARA's current block scanner.
wasm32 limits and wasm64
This line of work also led me to look at whether a
wasm64build would be practical.Even with the
userfaultfdapproach, awasm32guest is still fundamentally limited by its address space. In practice that means the maximum size of a block-scannable file is still somewhat less than 4 GB. I investigated whether moving towasm64would let that limit be lifted, but at the moment that is not practical because of upstream dependency issues. So for now that ceiling remains in place.I think it is worth being explicit about that limitation, even though I would have preferred to remove it.
Performance
It was also important to me that the Go bindings be as fast as possible, because my hope is to use them in production as a replacement for the C bindings rather than treating them as an experiment.
I spent a fair amount of time analysing and profiling the Go/WASM path to get it into a reasonable state. A number of the details in the runtime, particularly around synchronisation behaviour, came out of that work. The goal there was to reduce unnecessary copying and state movement between host and guest, and to make the normal scan path efficient enough to be taken seriously as a production option.
So while the pure-Go build and deployment system was one motivation, performance was equally important, if not even more so.
Testing
I also put a lot of effort into validating both the normal and experimental Go paths.
The Go module functionality tests have been factored into a shared reusable suite and are exercised through:
Scanner.ScanpathScanner.ScanReaderAtpathuserfaultfd-backedScanReaderAtpathThat is especially useful because modules tend to access the scanned data in a much less predictable way than the main scanner. Reusing the existing module fixture corpus against these paths is a much better stress test of the lazy-paging behaviour than a simple sequential scan would be.
Note: I ran the Go/WASM tests in Docker in both the standard and privileged
userfaultfdconfigurations.Why I am proposing this
What I wanted to do here was not just add functionality, but leave the code in a shape that is easier to work with afterwards.
In practical terms, I think this improves things by:
ScanReaderAt, performance, and the experimental allocator pathMaintenance
I appreciate that
go-wasmis a fairly substantial addition, and I can understand that you may be concerned about the maintenance burden.I am happy to make a clear commitment to maintaining the
go-wasmcode going forward. If there is concern about keeping that much code in-tree, I would also be willing to move it out of tree and maintain it separately instead.My preference would be to keep it in-tree, because it benefits from staying close to YARA-X itself, sharing tests and versioning, and being validated alongside the rest of the project. But if you would rather reduce the amount of code carried in the main repository, I completely understand.
Finally, I am fairly new to Rust as most of my work is done in Go. I took some lessons from the modifications you made to my previous PR in the hopes that this one is a little better. Even so, I apologise if my relative inexperience with Rust has caused me to produce any Rust anti-patterns.