Repository navigation
CAMEL-25531: camel-wasm - Run WASI Preview 1 command modules - #27678
allthingssecurity wants to merge 2 commits into
Conversation
The wasm endpoint now runs a WASI Preview 1 command module (a Rust program built for wasm32-wasip1, a Go program built with GOOS=wasip1 GOARCH=wasm, ...) instead of calling a function through the custom alloc/dealloc and JSON message ABI, which is removed from the endpoint. - The URI path is the module: wasm:classpath:upper.wasm. - The body is standard input, standard output becomes the body; the exit code and standard error are in CamelWasmExitCode and CamelWasmStderr. A non-zero exit fails the exchange with a WasmExitCodeException unless failOnNonZeroExit=false. - The module is parsed once per producer and every exchange runs on a new instance, so exchanges share no guest state and need no lock. - Environment only from environment.* and the environmentHeaders allow-list; fixed args; no preopened directories, no sockets. - Limits: maxMemoryPages, timeout (the module runs on a producer thread that is interrupted) and maxOutputSize for the buffered output. - WasiCommand runs a module once and is the place where the optional compiler and the language can plug in later. The wasm language is unchanged. Docs, catalog, endpoint and component DSL regenerated; 4.23 upgrade-guide entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
davsclaus
left a comment
There was a problem hiding this comment.
Thanks, this is a solid redesign: running WASI command modules with stdin/stdout is a much more natural contract than the custom alloc/JSON ABI, and the sandboxing defaults (no preopens, no inherited env, null streams) plus the 24 tests are very good to see.
One thing needs fixing before merge: the WASI random_get source (see inline). Two smaller points:
- argv[0] is the raw module location (inline), which can carry credentials for an
http:resource. - The PR body still contains the section "Notes (for the author, remove before opening)" referring to a local
findings/wasm-evidence/...path; please remove it.
Questions (not blocking):
WasmProducer:message.getBody(InputStream.class)returns null for a body without an InputStream converter (e.g. a POJO), which silently becomes empty stdin. WouldgetMandatoryBody(InputStream.class)for a non-null body be better?- Timeout mode uses an unbounded cached pool; a module stuck in a host call that ignores interrupts keeps its thread. Acceptable, or worth a cap?
Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying. It is a static review against the project conventions and does not replace static analysis or specialized review tools.
…, file name as argument zero, fail on a body that is not a stream - WASI random_get is served by a shared SecureRandom instead of Endive's default ThreadLocalRandom - argument zero is the file name of the module, not its location, which can carry credentials or host paths - a non-null body that cannot be converted to an InputStream fails the exchange instead of becoming empty standard input Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🧪 CI tested the following changed modules:
🔬 Scalpel shadow comparison — Scalpel: 9 of 704 tested, 3 compile-only — current: 9 all testedMaveniverse Scalpel detected 9 affected modules (current approach: 9). Skip-tests mode would test 9 modules (5 direct + 0 downstream), skip tests for 3 (generated code, meta-modules) Modules Scalpel would test (9)
Modules with tests skipped (3)
All tested modules (36 modules, 5m 58s total)Total reactor time: 5m 58s
Top 20 slowest modules:
|
Claude Code on behalf of allthingssecurity |
davsclaus
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. All review points are addressed: random_get is served by a shared SecureRandom (with tests on the WASI options and a module calling random_get), argument zero is now only the file name of the module (a stronger choice than sanitizing the URI), the notes are gone from the description, and a body that cannot be read as a stream now fails the exchange instead of becoming empty input. LGTM once CI is green.
Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying. It is a static review against the project conventions and does not replace static analysis or specialized review tools.
|
pending this for after the 4.23 release |
Description
CAMEL-25531
Part 1 of 4 of the camel-wasm redesign agreed with @davsclaus on Zulip: the
wasmendpoint now runs WASI Preview 1 command modules instead of calling a function through the customalloc/deallocand JSON message ABI. A Rust program built forwasm32-wasip1or a Go program built withGOOS=wasip1 GOARCH=wasmcan be used as it is.Author: Shashank Mohan Jain (@allthingssecurity), working with Claude Code (Claude Opus 5.5).
What changes
wasm:classpath:upper.wasm(any Camel resource: classpath,file:,http:...). The function name and themoduleoption are gone.InputStreamfails the exchange withInvalidPayloadException); standard output becomes the body (byte[]); the exit code is inCamelWasmExitCode(Integer) and standard error inCamelWasmStderr(UTF-8). A non-zero exit fails the exchange with a newWasmExitCodeException(exit code + stderr, body unchanged) unlessfailOnNonZeroExit=false. Both headers are removed before every run, so a value sent with the message, or left by an earlierwasmstep, never survives (on success, non-zero exit, trap, output limit or timeout). Other headers are kept and are not passed to the module.doInit, so a bad module or bad options fail the route start) and every exchange runs on a new Endive instance, dropped afterwards. Exchanges share no guest memory or globals, there is no lock, and a trap or a timeout leaves nothing behind in a shared instance.args(fixed by the route, argument zero is the file name of the module, such asupper.wasm: the location itself can carry credentials or host paths and is not passed on), WASIrandom_getbacked by a sharedSecureRandom(Endive's default isThreadLocalRandom), environment variables only fromenvironment.NAME=valueand the headers listed inenvironmentHeaders(an allow-list of names, looked up like any Camel header; a missing header is left unset; a value with a NUL fails the exchange). Never the JVM environment. No preopened directories, no sockets. Clock and random are the host's. The standard streams are reported as non-terminals (Endive reports terminals unless told otherwise), so a program that checks for a terminal does not add colours or other terminal output to the body.maxMemoryPages: EndiveMemoryLimitson the module's memory; growing past it fails inside the guest. Lower than the module's initial memory is rejected at start.timeout: the module runs on a cached thread pool of the producer (created indoStartonly when a timeout is set,shutdownNowindoStop) and the route thread waits on the future. On expiry the worker is interrupted; Endive checks the interrupt flag on calls and backward branches, so a spinning guest stops at its next iteration. The exchange fails withExchangeTimedOutException. Interrupting a producer thread keeps the interrupt flag away from route threads. No thread hop without a timeout.maxOutputSize: cap on the buffered standard output and, separately, standard error. Exceeding it stops the module (the buffer throws aRuntimeException, which Endive does not turn into anEIOfor the guest) and fails the exchange with a message naming the stream and the option.panic):CamelExchangeExceptioncaused by Endive'sTrapException, with the start of stderr in the message (Rust prints the panic message there) andCamelWasmStderrset; a hint is added when the memory is at its maximum.New public types:
org.apache.camel.wasm.WasiCommand(runs a parsed command module once, given stdin, stdout, stderr and the environment: the place where PR 2 adds the compiled machine factory and PR 3 can reuse it for the language),org.apache.camel.component.wasm.WasmExitCodeException, the two header constants inWasm.Headers,WasmEndpoint.getModule(). Removed:WasmComponent.get/setModule,WasmConfiguration.get/setModule, the endpoint'sfunctionName,WasmSupport.deserialize(only the old producer used it; the language still usesserialize);WasmProducernow takes aWasmEndpoint.WasmConfiguration.copy()now copies theenvironmentmap, so endpoints never share it.Not changed: the
wasmlanguage (it still usesWasmFunctionand the custom ABI; PR 3 rebases or drops it). The 4.23 upgrade-guide note about a failed call discarding the instance now only mentions thewasmexpression, since the endpoint no longer keeps an instance.Security model
Modules stay trusted route code, as in the security model page; the docs say that the limits are robustness controls, not a sandbox. Bodies and headers stay untrusted data: the body only reaches the module as standard input, headers only through the explicit allow-list, arguments are fixed by the route. No option relaxes a default, so none is marked
insecure:*.Docs and generated files
wasm-component.adocrewritten for the WASI model: writing a module (Rust and Go), how a run works, what the module can access, limits (with the security-model sentence), an error table, performance, Java/XML/YAML examples. Catalog copy andcomponents/wasm.jsonregenerated with theprepare-catalogmojo (identical to the component files); endpoint DSL (WasmEndpointBuilderFactory,StaticEndpointBuilders,EndpointHeaderBuilders) and component DSL description regenerated with thegenerate-endpoint-dsl/generate-component-dslmojos.=== camel-wasm - the endpoint runs WASI command modules (Breaking change), next to the existing camel-wasm entries: what changed and how to migrate (turn the function into a program reading stdin and writing stdout, build it for WASI Preview 1).Dependencies
Adds
run.endive:wasi:${endive-version}(1.1.0, Apache-2.0, same project as the runtime already used; bringsrun.endive:log). Endive alignment in the parent POM (importing the Endive BOM so camel-quickjs'swasi1.0.0 via quickjs4j 0.1.0 and camel-opa'sruntime/compiler1.0.0 via opa-java-wasm 1.1.0 move to 1.1.0) is not in this PR: it changes the Endive versions camel-opa and camel-quickjs run with and the generatedcamel-dependencies, so it needs their test suites and belongs with PR 2, where the compiler dependency (the one camel-opa already brings at 1.0.0) arrives. This PR does not add a new split: camel-quickjs already runs quickjs4j'swasi1.0.0 onruntime1.1.0, and the current SBOM already lists Endiveruntimeandwasmat both 1.0.0 and 1.1.0. @davsclaus if you would rather have the alignment in this PR, I will add it here.Tests
WasmComponentTestis rewritten: 26 test methods (37 runs with the parameterized ones), plusWasiCommandTest. The modules are written in WAT in the test and compiled when the test runs withrun.endive:wabt(already a test dependency), so no binary is added; the existingfunctions.wasmis used to check that a module without_startis rejected (and keeps servingWasmLanguageTest).WasmExitCodeExceptionwith code and stderr, body unchanged, headers set; same withfailOnNonZeroExit=false(body = stdout); exit 0 throughproc_exitwith stderrTrapExceptionand the stderr text,CamelWasmStderrset, inboundCamelWasmExitCoderemovedLEVEL=debugandTENANT=acmefromenvironment.LEVEL+environmentHeaders=TENANT,REGIONwhile the message also carriesSECRETandCamelHttpUri(and no JVM variables); header with NUL fails; a name in both options fails the startargs.wasm; the program name for file, classpath, ref, http(s) with user info and query, base64 and gzip locations; unbalanced quote fails the startInputStreamconverter fails the exchange withInvalidPayloadExceptionrandom_getreturns 32 bytes that differ between runs;WasiCommandTestasserts that the WASI options carry aSecureRandomExchangeTimedOutException, no result headers), then awaits (Awaitility, 10 s) until no producer thread runs guest code; timeout endpoint across a route stop/startmaxMemoryPages=4: the guest that grows its memory ends at 4 pages (17 without the cap); a cap below the initial memory fails the startmaxOutputSizeon stdout (1000 bytes pass at 1000, fail at 999) and on stderr_start, module importing outsidewasi_snapshot_preview1: start failsSensitivity: with the header removal deleted, the interrupt replaced by
cancel(false), the memory limit not applied and the output cap disabled, 6 tests fail (resultHeadersSentWithTheMessageAreReplaced,trapFailsTheExchangeWithStandardError,timeoutInterruptsASpinningModule(the spinning guest is still running after 10 s),maxMemoryPagesLimitsMemoryGrowth,maxOutputSizeLimitsStandardOutput,maxOutputSizeLimitsStandardError). With Endive's default terminal streams,standardStreamsAreNotTerminalsfails (222instead of000).camel-wasm with
install(formatter, import sort, generation): 47 tests, 0 failures (WasmComponentTest37 +WasmLanguageTest6 +WasmFunctionTest3 +WasiCommandTest1).Performance
Measured through a route (
ProducerTemplate.requestBody->direct:->wasm:), interpreted, 1 KiB ASCII body, Apple M4 Pro, JDK 21, Endive 1.1.0; local probe, not committed:GOOS=wasip1program (io.ReadAll+bytes.ToUpper)no_stdRust WASI command (ASCII upper-case)A fresh instance per exchange is cheap for a small module; a standard Go program spends most of each run initializing the Go runtime again. Per-exchange cost of this design vs the old endpoint: one instantiation (data segments copied, Go's are large) plus a WASI context, no lock. The optional compiler (PR 2) cut the Go case to about 5 ms per exchange in the earlier prototype; TinyGo/Rust/C guests avoid most of it. This is why PR 4 will show both Rust and Go and state the numbers.
Plan
compile=true, off by default because of native images), docs for the Endive build-time compiler plugin, Endive version alignment in the parent POM.wasmlanguage: rebase it onWasiCommand, or drop it.@davsclaus please review.
Target
mainbranch)Tracking
Apache Camel coding standards and style
mvn clean install -DskipTestslocally from root folder and I have committed all auto-generated changes.(I built and tested camel-wasm with
install, including the formatter and import-sort plugins, and regenerated the catalog copy and the endpoint and component DSL with the camel-package-maven-plugin mojos. I did not run the full root build.)AI-assisted contributions
Co-authored-bytrailers) and the PR description identifies the AI tool used.Prepared by Shashank Mohan Jain (@allthingssecurity) with Claude Code (Claude Opus 5.5). The commit carries a
Co-Authored-Bytrailer.Claude Code on behalf of allthingssecurity
🤖 Generated with Claude Code