allthingssecurity commented on code in PR #27678: URL: https://github.com/apache/camel/pull/27678#discussion_r4240515837
########## components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java: ########## @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.wasm; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.TrapException; +import run.endive.wasi.WasiExitException; +import run.endive.wasi.WasiOptions; +import run.endive.wasi.WasiPreview1; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.Export; +import run.endive.wasm.types.ExportSection; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.MemoryLimits; +import run.endive.wasm.types.MemorySection; + +/** + * Runs a WASI Preview 1 command module (a program with a {@code _start} function, such as a Rust program built for + * {@code wasm32-wasip1} or a Go program built with {@code GOOS=wasip1 GOARCH=wasm}) to completion. + * <p/> + * The module is parsed once by the caller and every {@link #run} creates a new instance of it, so runs never share + * guest memory or globals and need no lock. A run gets exactly the standard streams, arguments and environment + * variables it is given: no preopened directories (so no files) and no sockets. Clock and random numbers are the + * host's. + */ +public final class WasiCommand { + + private static final String START = "_start"; + private static final String WASI_MODULE = "wasi_snapshot_preview1"; + + private final WasmModule module; + private final List<String> arguments; + private final MemoryLimits memoryLimits; + + /** + * @param module the parsed module + * @param arguments the program's arguments, the program name first + * @param maxMemoryPages the maximum number of 64 KiB pages the module's memory may grow to, or 0 to keep + * the maximum the module declares + * @throws IllegalArgumentException if the module is not a WASI Preview 1 command module, or maxMemoryPages is lower + * than the initial memory the module declares + */ + public WasiCommand(WasmModule module, List<String> arguments, int maxMemoryPages) { + this.module = Objects.requireNonNull(module); + this.arguments = List.copyOf(arguments); + validate(module); + this.memoryLimits = memoryLimits(module, maxMemoryPages); + } + + /** + * Runs the program once, on a new instance, on the calling thread. + * <p/> + * A {@link RuntimeException} thrown by one of the given streams stops the program and is rethrown. + * + * @param stdin the program's standard input + * @param stdout receives the program's standard output + * @param stderr receives the program's standard error + * @param environment the program's complete environment + * @return the exit code: 0 when {@code _start} returned, otherwise the + * value passed to {@code proc_exit} + * @throws TrapException if the program trapped (for example a Rust panic) + * @throws run.endive.runtime.WasmInterruptedException if the calling thread was interrupted + */ + public int run(InputStream stdin, OutputStream stdout, OutputStream stderr, Map<String, String> environment) { + // built on the thread that runs the program: the default random number generator is the thread's own. + // Endive reports the standard streams as terminals unless told otherwise; they are not, and a program that + // sees a terminal may add terminal output, such as colours, to the message body + WasiOptions.Builder options = WasiOptions.builder() Review Comment: Confirmed in the Endive 1.1.0 jar: `WasiOptions$Builder.<init>` sets `random` to `ThreadLocalRandom.current()` and `WasiPreview1.randomGet` calls `random.nextBytes`. Fixed in cd87fa0fb8d8: `WasiCommand` now passes one shared `SecureRandom` with `withRandom(...)`. Tests: `WasiCommandTest.randomGetIsServedByASecureRandom` asserts that the WASI options of a run carry a `SecureRandom`, and `WasmComponentTest.randomGetReturnsBytesOfTheHost` runs a module that calls `random_get` (32 bytes, different on each run). The misleading comment is gone and the doc line now says that `random_get` returns bytes from a `java.security.SecureRandom`. _Claude Code on behalf of allthingssecurity_ ########## components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java: ########## @@ -16,76 +16,335 @@ */ package org.apache.camel.component.wasm; +import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; -import org.apache.camel.Endpoint; +import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; -import org.apache.camel.spi.Resource; -import org.apache.camel.spi.ResourceLoader; +import org.apache.camel.ExchangeTimedOutException; +import org.apache.camel.Message; import org.apache.camel.support.DefaultProducer; -import org.apache.camel.support.PluginHelper; -import org.apache.camel.wasm.WasmFunction; -import org.apache.camel.wasm.WasmSupport; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.URISupport; +import org.apache.camel.wasm.WasiCommand; +import org.apache.camel.wasm.Wasm; +import run.endive.runtime.TrapException; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; +/** + * Runs a WASI Preview 1 command module once per exchange: the message body is the standard input of the module and its + * standard output becomes the message body. + * <p/> + * The module is parsed once, when the producer is initialized, and every exchange runs on a new instance of it, so + * exchanges never share guest memory or globals and need no lock. The instance is dropped after its run, whatever the + * outcome. + */ public class WasmProducer extends DefaultProducer { - private final String functionModule; - private final String functionName; + private static final int MAX_STDERR_IN_MESSAGE = 256; - private WasmModule module; - private WasmFunction function; + private WasiCommand command; + private Map<String, String> environment = Collections.emptyMap(); + private List<String> environmentHeaders = Collections.emptyList(); + private volatile ExecutorService executor; - public WasmProducer(Endpoint endpoint, String functionModule, String functionName) throws Exception { + public WasmProducer(WasmEndpoint endpoint) { super(endpoint); + } - this.functionModule = functionModule; - this.functionName = functionName; + @Override + public WasmEndpoint getEndpoint() { + return (WasmEndpoint) super.getEndpoint(); } @Override - public void doInit() throws Exception { - final ResourceLoader rl = PluginHelper.getResourceLoader(getEndpoint().getCamelContext()); - final Resource res = rl.resolveResource(this.functionModule); + protected void doInit() throws Exception { + super.doInit(); + + WasmEndpoint endpoint = getEndpoint(); + WasmConfiguration conf = endpoint.getConfiguration(); + + Map<String, String> env = new LinkedHashMap<>(); + if (conf.getEnvironment() != null) { + for (Map.Entry<String, Object> e : conf.getEnvironment().entrySet()) { + String value = e.getValue() != null ? e.getValue().toString() : ""; + env.put(checkName(e.getKey()), checkNoNul("environment variable " + e.getKey(), value)); + } + } + List<String> headers = new ArrayList<>(); + if (ObjectHelper.isNotEmpty(conf.getEnvironmentHeaders())) { + for (String header : conf.getEnvironmentHeaders().split(",")) { + String name = checkName(header.trim()); + if (env.containsKey(name)) { + throw new IllegalArgumentException( + "Environment variable " + name + " is configured both in environment and environmentHeaders"); + } + headers.add(name); + } + } + List<String> arguments = arguments(endpoint.getModule(), conf.getArgs()); Review Comment: Confirmed: argument zero was `endpoint.getModule()` unchanged. Changed in cd87fa0fb8d8 to the file name of the module (`upper.wasm` for `file:/opt/modules/upper.wasm`), without scheme, directories, host, user info or query. I chose the file name over `URISupport.sanitizeUri`: sanitizing only masks user info and the query keys it knows as secrets, so a token under another key, the host and the host directories would still reach the module, and a WASI program expects its program name in argv[0] (it shows up in usage and error messages). `base64:` and `gzip:` locations, and a location without a file name, get `module.wasm`. The `args` description, the component doc and the generated catalog/DSL copies are updated. Tests: `argumentsFromTheRoute` now expects `args.wasm`, and `argumentZeroIsTheFileNameOfTheModule` covers 11 locations, including `https://user:[email protected]/mods/upper.wasm?token=abc123`. _Claude Code on behalf of allthingssecurity_ -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
