From b87e712bfc07f4d6f7b100275e9e6e88cd792dae Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Wed, 18 Jun 2025 13:36:39 -0400 Subject: [PATCH] feat: improved WASI sandboxing Implements a restricted subset of the WASI ABI (wasi_snapshot_preview1) to support proxy-wasm modules that require base level WASI functionality. This implementation adds a ABI_WASI class to handle WASI-specific functions: - Supports core WASI functions required by proxy-wasm spec - Implements fd_write for stdout/stderr logging - Adds support for clock, random, environment, and args functions This should result in a better sandboxing of the guest modules. --- .../roastedroot/proxywasm/PluginFactory.java | 15 - .../roastedroot/proxywasm/internal/ABI.java | 14 + .../proxywasm/internal/ABI_WASI.java | 375 ++++++++++++++++++ .../proxywasm/internal/ProxyWasm.java | 50 +-- 4 files changed, 390 insertions(+), 64 deletions(-) create mode 100644 proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI_WASI.java diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java index fd241df..d1cdc16 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java @@ -5,7 +5,6 @@ import com.dylibso.chicory.runtime.ImportMemory; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Machine; -import com.dylibso.chicory.wasi.WasiOptions; import com.dylibso.chicory.wasm.WasmModule; import io.roastedroot.proxywasm.internal.ProxyWasm; import java.net.URI; @@ -319,20 +318,6 @@ public PluginFactory.Builder withMachineFactory( return this; } - /** - * Configures WebAssembly System Interface (WASI) options for the plugin instance. - * WASI provides a standard interface for WASM modules to interact with the underlying operating system - * for tasks like file system access, environment variables, etc. While Proxy-WASM defines its own ABI, - * some modules might also utilize WASI features. - * - * @param options The {@link WasiOptions} to configure for the WASI environment. - * @return this {@code Builder} instance for method chaining. - */ - public PluginFactory.Builder withWasiOptions(WasiOptions options) { - proxyWasmBuilder.withWasiOptions(options); - return this; - } - /** * Configures whether the plugin instance should be shared across multiple host requests or contexts. * diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI.java index 6ba2d46..8ad6f2c 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI.java @@ -8,6 +8,7 @@ import com.dylibso.chicory.annotations.HostModule; import com.dylibso.chicory.annotations.WasmExport; import com.dylibso.chicory.runtime.ExportFunction; +import com.dylibso.chicory.runtime.HostFunction; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Memory; import com.dylibso.chicory.runtime.WasmRuntimeException; @@ -1921,4 +1922,17 @@ void proxyOnForeignFunction(int contextId, int functionId, int argumentsSize) { } proxyOnForeignFunctionFn.apply(contextId, functionId, argumentsSize); } + + @WasmExport + void emscriptenNotifyMemoryGrowth(int size) {} + + public HostFunction[] toHostFunctions() { + var functions = new ArrayList<>(List.of(ABI_ModuleFactory.toHostFunctions(this))); + + HostFunction[] wasiFunctions = new ABI_WASI(handler).toHostFunctions(); + functions.addAll(List.of(wasiFunctions)); + functions.addAll(List.of(Helpers.withModuleName(wasiFunctions, "wasi_unstable"))); + + return functions.toArray(new HostFunction[0]); + } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI_WASI.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI_WASI.java new file mode 100644 index 0000000..bd22831 --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ABI_WASI.java @@ -0,0 +1,375 @@ +package io.roastedroot.proxywasm.internal; + +import com.dylibso.chicory.annotations.Buffer; +import com.dylibso.chicory.annotations.HostModule; +import com.dylibso.chicory.annotations.WasmExport; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.Memory; +import com.dylibso.chicory.runtime.WasmRuntimeException; +import com.dylibso.chicory.wasi.WasiOptions; +import com.dylibso.chicory.wasi.WasiPreview1; +import io.roastedroot.proxywasm.LogLevel; +import io.roastedroot.proxywasm.WasmException; +import java.nio.charset.StandardCharsets; + +/** + * Implements a restricted subset of the WASI ABI, specifically so that a WASM module can + * be linked with WASI. Only the functions documented by the proxy-wasm spec will work. All other + * function will result in errors. + */ +@HostModule("wasi_snapshot_preview1") +public class ABI_WASI { + + // We only need this field so we can delegate a few function calls. Maybe we should just copy + // over those + // implementations. + final WasiPreview1 wasi = + WasiPreview1.builder().withOptions(WasiOptions.builder().build()).build(); + + private final Handler handler; + + public ABI_WASI(Handler handler) { + this.handler = handler; + } + + //////////////////////////////////////////////////////////////////////////// + // These functions are documented by the proxy-wasm spec. Implement them + // as described there. + //////////////////////////////////////////////////////////////////////////// + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1fd_write + */ + @WasmExport + public int fdWrite(Memory memory, int fd, int iovs, int iovsLen, int nwrittenPtr) { + LogLevel level = null; + switch (fd) { + case 1: + // stdout: log at info level + level = LogLevel.INFO; + break; + case 2: + // stderr: log at error level + level = LogLevel.ERROR; + break; + default: + // BADF + return 8; + } + + var totalWritten = 0; + for (var i = 0; i < iovsLen; i++) { + var base = iovs + (i * 8); + var iovBase = memory.readInt(base); + var iovLen = memory.readInt(base + 4); + var data = memory.readBytes(iovBase, iovLen); + try { + handler.log(level, new String(data, StandardCharsets.UTF_8)); + totalWritten += iovLen; + } catch (WasmException e) { + return 29; // EIO + } + } + + memory.writeI32(nwrittenPtr, totalWritten); + return 0; // ESUCCESS + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1clock_time_get + */ + @WasmExport + public int clockTimeGet(Memory memory, int clockId, long precision, int resultPtr) { + return wasi.clockTimeGet(memory, clockId, precision, resultPtr); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1random_get + */ + @WasmExport + public int randomGet(Memory memory, int buf, int bufLen) { + return wasi.randomGet(memory, buf, bufLen); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1environ_sizes_get + */ + @WasmExport + int environSizesGet(Memory memory, int return_num_elements, int return_buffer_size) { + return wasi.environSizesGet(memory, return_num_elements, return_buffer_size); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1environ_get + */ + @WasmExport + int environGet(Memory memory, int return_array, int return_buffer) { + return wasi.environGet(memory, return_array, return_buffer); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1args_sizes_get + */ + @WasmExport + public int argsSizesGet(Memory memory, int argc, int argvBufSize) { + return wasi.argsSizesGet(memory, argc, argvBufSize); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1args_get + */ + @WasmExport + int argsGet(Memory memory, int return_argv, int return_argv_size) { + return wasi.argsGet(memory, return_argv, return_argv_size); + } + + /** + * implements: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.2.1/README.md#wasi_snapshot_preview1proc_exit + */ + @WasmExport + void procExit(int exit_code) { + throw new WasmRuntimeException("unsupported"); + } + + public HostFunction[] toHostFunctions() { + return ABI_WASI_ModuleFactory.toHostFunctions(this); + } + + //////////////////////////////////////////////////////////////////////////// + // The following function should technically not be getting called, but + // we have seen some go modules that call them, so we implement them so that they + // will not crash out. + //////////////////////////////////////////////////////////////////////////// + @WasmExport + public int pollOneoff( + Memory memory, int inPtr, int outPtr, int nsubscriptions, int neventsPtr) { + memory.writeI32(neventsPtr, 0); + return 0; // ESUCCESS + } + + @WasmExport + public int fdFdstatGet(Memory memory, int fd, int buf) { + return 8; // EBADF + } + + @WasmExport + public int fdPrestatGet(Memory memory, int fd, int buf) { + return 8; // EBADF + } + + //////////////////////////////////////////////////////////////////////////// + // Have not seen these get called so just throw an exception to simplify + // the implementation. If you see these getting called, please open an issue + //////////////////////////////////////////////////////////////////////////// + + @WasmExport + int fdClose(int fd) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int adapterCloseBadfd(int fd) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int adapterOpenBadfd(int fd) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int clockResGet(Memory memory, int clockId, int resultPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdAdvise(int fd, long offset, long len, int advice) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdAllocate(int fd, long offset, long len) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdDatasync(int fd) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdFdstatSetFlags(int fd, int flags) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdFdstatSetRights(int fd, long rightsBase, long rightsInheriting) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdFilestatGet(Memory memory, int fd, int buf) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdFilestatSetSize(int fd, long size) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdFilestatSetTimes(int fd, long accessTime, long modifiedTime, int fstFlags) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdPread(Memory memory, int fd, int iovs, int iovsLen, long offset, int nreadPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdPrestatDirName(Memory memory, int fd, int path, int pathLen) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdPwrite( + Memory memory, int fd, int iovs, int iovsLen, long offset, int nwrittenPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdRead(Memory memory, int fd, int iovs, int iovsLen, int nreadPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdReaddir( + Memory memory, int dirFd, int buf, int bufLen, long cookie, int bufUsedPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdRenumber(int from, int to) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdSeek(Memory memory, int fd, long offset, int whence, int newOffsetPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdSync(int fd) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int fdTell(Memory memory, int fd, int offsetPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathCreateDirectory(int dirFd, @Buffer String rawPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathFilestatGet( + Memory memory, int dirFd, int lookupFlags, @Buffer String rawPath, int buf) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathFilestatSetTimes( + int fd, + int lookupFlags, + @Buffer String rawPath, + long accessTime, + long modifiedTime, + int fstFlags) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathLink( + int oldFd, + int oldFlags, + @Buffer String rawOldPath, + int newFd, + @Buffer String rawNewPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathOpen( + Memory memory, + int dirFd, + int lookupFlags, + @Buffer String rawPath, + int openFlags, + long rightsBase, + long rightsInheriting, + int fdFlags, + int fdPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathReadlink( + Memory memory, int dirFd, @Buffer String rawPath, int buf, int bufLen, int bufUsedPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathRemoveDirectory(int dirFd, @Buffer String rawPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathRename( + int oldFd, @Buffer String oldRawPath, int newFd, @Buffer String newRawPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathSymlink(@Buffer String oldRawPath, int dirFd, @Buffer String newRawPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int pathUnlinkFile(int dirFd, @Buffer String rawPath) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int procRaise(int sig) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int schedYield() { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int sockAccept(int sock, int fdFlags, int roFdPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int sockRecv( + int sock, int riDataPtr, int riDataLen, int riFlags, int roDataLenPtr, int roFlagsPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int sockSend(int sock, int siDataPtr, int siDataLen, int siFlags, int retDataLenPtr) { + throw new WasmRuntimeException("unsupported"); + } + + @WasmExport + public int sockShutdown(int sock, int how) { + throw new WasmRuntimeException("unsupported"); + } +} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ProxyWasm.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ProxyWasm.java index a6ad796..54e8df9 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ProxyWasm.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/ProxyWasm.java @@ -3,20 +3,15 @@ import static io.roastedroot.proxywasm.internal.Helpers.len; import com.dylibso.chicory.runtime.ByteArrayMemory; -import com.dylibso.chicory.runtime.HostFunction; import com.dylibso.chicory.runtime.ImportMemory; import com.dylibso.chicory.runtime.ImportValues; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Machine; -import com.dylibso.chicory.wasi.WasiOptions; -import com.dylibso.chicory.wasi.WasiPreview1; import com.dylibso.chicory.wasm.WasmModule; import com.dylibso.chicory.wasm.types.MemoryLimits; -import com.dylibso.chicory.wasm.types.ValueType; import io.roastedroot.proxywasm.StartException; import java.io.Closeable; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -25,7 +20,6 @@ public final class ProxyWasm implements Closeable { private final ABI abi; - private final WasiPreview1 wasi; private final AtomicInteger nextContextID = new AtomicInteger(1); private Context pluginContext; @@ -42,7 +36,6 @@ public final class ProxyWasm implements Closeable { private ProxyWasm(Builder other) throws StartException { this.pluginHandler = Objects.requireNonNullElse(other.pluginHandler, new Handler() {}); - this.wasi = other.wasi; this.abi = other.abi; this.abi.setHandler(createImportsHandler()); @@ -196,9 +189,6 @@ public void close() { return; } this.pluginContext.close(); - if (wasi != null) { - wasi.close(); - } } public static ProxyWasm.Builder builder() { @@ -262,11 +252,9 @@ ABI abi() { public static class Builder implements Cloneable { private final ABI abi = new ABI(); - private WasiPreview1 wasi; private Handler pluginHandler; private ImportMemory memory; - private WasiOptions wasiOptions; private boolean start = true; private Function machineFactory; @@ -280,10 +268,6 @@ protected Builder clone() { } } - public HostFunction[] toHostFunctions() { - return ABI_ModuleFactory.toHostFunctions(abi); - } - public Builder withStart(boolean start) { this.start = start; return this; @@ -299,11 +283,6 @@ public ProxyWasm.Builder withImportMemory(ImportMemory memory) { return this; } - public ProxyWasm.Builder withWasiOptions(WasiOptions options) { - this.wasiOptions = options; - return this; - } - public ProxyWasm.Builder withMachineFactory(Function machineFactory) { this.machineFactory = machineFactory; return this; @@ -328,30 +307,7 @@ public ProxyWasm build(Instance.Builder instanceBuilder) throws StartException { } imports.addMemory(Objects.requireNonNullElseGet(memory, this::defaultImportMemory)); - imports.addFunction(toHostFunctions()); - imports.addFunction( - new HostFunction( - "env", - "emscripten_notify_memory_growth", - List.of(ValueType.I32), - List.of(), - (inst, args) -> { - return null; - })); - - wasi = - WasiPreview1.builder() - .withOptions( - Objects.requireNonNullElseGet( - wasiOptions, - () -> - WasiOptions.builder() - .inheritSystem() - .withArguments(List.of()) - .build())) - .build(); - imports.addFunction(wasi.toHostFunctions()); - imports.addFunction(Helpers.withModuleName(wasi.toHostFunctions(), "wasi_unstable")); + imports.addFunction(abi.toHostFunctions()); var instance = instanceBuilder @@ -368,9 +324,5 @@ ImportMemory defaultImportMemory() { "memory", new ByteArrayMemory(new MemoryLimits(2, MemoryLimits.MAX_PAGES))); } - - WasiOptions defaultWasiOptions() { - return WasiOptions.builder().inheritSystem().build(); - } } }