execute_wasm() experimental: three unfinished input validations (TODO #26092) unvalidated export signatures, unbounded output reads, silent memory limit bypass
- Dominant language
- Java
- Stars
- 25.8k
- Forks
- 4.6k
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 72
Description
### Description of the bug:
`repository_ctx.execute_wasm()`/`load_wasm()` (experimental, behind `--experimental_repository_ctx_execute_wasm`, tracked under #26092) has several open TODOs in `StarlarkWasmModule.java` marking unfinished validation. I tested the three most concerning ones against Bazel 9.2.0 and all three reproduce as described below, confirmed live on a fresh install. None of these are memory-corruption or RCE, the outer executor/exception wrapper around the call catches essentially anything thrown inside it, so failures degrade to an error message rather than crashing the invocation.
**1. The exported function's signature is never validated against what Bazel expects.**
Bazel calls the exported function via a raw ABI call (4x `i32` args, 1x `i32` result expected), but nothing checks the export actually has that type. I built a module exporting `run` as `(param i64) (result i64 i64)` instead completely different arity and types. Bazel didn't reject it. The runtime called it anyway and Bazel read the first return value back as a return code:
```
=== Control (correct signature) ===
run -> return_code=0 output="AAAA" error=""
=== Mismatched signature ===
run -> return_code=8 output="" error=""
```
`8` is simply the value of `input_ptr` (from the allocator) being echoed back through a function that has nothing to do with the real `run` contract. This is `TODO: #26092 Validate execFn has the expected signature?` (`StarlarkWasmModule.java:239`). A module with a mismatched signature isn't rejected it produces plausible-looking, semantically meaningless output, reported identically to a genuine success.
**2. `memory_limit` does not bound the host-side output-buffer allocation.**
After `run` returns, Bazel reads `outputPtr`/`outputLen` back from WASM memory (both attacker-controlled a module writes them into a buffer it owns) and allocates a buffer sized directly from that value. I set `outputLen = 2147483647` (`i32::MAX`) against `memory_limit = 65536` (one page):
```
run_overflow_len -> return_code=-1 output="" error="Error executing run_overflow_len: Requested array size exceeds VM limit"
```
It's caught, so it degrades gracefully here but the ceiling on this allocation attempt is the JVM's own max array size (~2^31−1 bytes), not `memory_limit`. A module could pick a value comfortably under that ceiling (a few hundred MB) and this path would very likely just succeed, consuming real host memory with no relationship to the configured limit. For contrast, a genuinely out-of-range pointer at a *reasonable* length is handled correctly:
```
run_oob_ptr -> return_code=-1 output="" error="Error executing run_oob_ptr: out of bounds memory access: attempted to access address: 1000000 but limit is: 65536 and size: 16"
```
So the bounds-checking machinery clearly exists and works it's specifically the very-large-`outputLen` path that bypasses it by failing at the allocation step, before any bounds check runs.
**3. A module's declared initial memory silently gets clamped against `memory_limit`, with no warning.**
I declared 100 pages (~6.5 MiB) of initial memory and ran with `memory_limit` set to one page:
```
run -> return_code=0 output="\x00\x00\x00\x00" error=""
```
No error, no warning. This is `TODO: #26092 Should probably throw an exception. The execution will likely fail anyway, and throwing an exception from this point would provide more relevant details.` (`StarlarkWasmModule.java:317`).
I raised finding 1 with the Google OSS VRP panel; it was closed (Won't Fix/Infeasible) on the reasoning that Bazel's threat model treats the build environment, its inputs, and its toolchain as fully trusted, and this falls inside that model consistent with how they've assessed related config-trust findings before. That's a defensible, consistently-applied position. Filing all three here per their own suggestion in that closure, so it's visible to the community independent of the VRP determination, the panel's note also explicitly says this kind of write-up is welcome as a public issue for broader discussion of the ecosystem's robustness.
### Which category does this issue belong to?
Configurability
### What's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.
`MODULE.bazel`:
```python
module(name = "wasm_execute_poc", version = "0.1")
wasm_repro_ext = use_extension("//:repro.bzl", "repro_extension")
use_repo(wasm_repro_ext, "wasm_repro")
```
`.bazelrc`:
```
common --experimental_repository_ctx_execute_wasm
```
`repro.bzl`:
```python
def _run_case(rctx, label, name, exec_fn, memory_limit):
print("=== %s ===" % name)
mod = rctx.load_wasm(label)
result = rctx.execute_wasm(mod, exec_fn, input = "", memory_limit = memory_limit)
print("%s -> return_code=%s output=%r error=%r" % (
exec_fn, result.return_code, result.output, result.error_message))
def _repro_impl(rctx):
_run_case(rctx, Label("//:control.wasm"), "Control", "run", 1048576)
_run_case(rctx, Label("//:bad_signature.wasm"), "Test 1: mismatched signature", "run", 1048576)
_run_case(rctx, Label("//:oob_output.wasm"), "Test 2a: OOB pointer", "run_oob_ptr", 65536)
_run_case(rctx, Label("//:oob_output.wasm"), "Test 2b: overflow length", "run_overflow_len", 65536)
_run_case(rctx, Label("//:oversized_initial_memory.wasm"), "Test 3: oversized initial memory", "run", 65536)
rctx.file("BUILD", 'filegroup(name = "wasm_repro", srcs = [])')
repro = repository_rule(implementation = _repro_impl)
def _ext_impl(module_ctx):
repro(name = "wasm_repro")
repro_extension = module_extension(implementation = _ext_impl)
```
Case 1: mismatched signature (`.wat`):
```wat
(module
(memory (export "memory") 1)
(func $run (export "run") (param $x i64) (result i64 i64)
(local.get $x) (local.get $x)))
```
Case 2: oversized output length:
```wat
(func $run_overflow_len (export "run_overflow_len")
(param $input_ptr i32) (param $input_len i32)
(param $output_ptr_ptr i32) (param $output_len_ptr i32) (result i32)
(i32.store (local.get $output_ptr_ptr) (i32.const 65500))
(i32.store (local.get $output_len_ptr) (i32.const 2147483647))
(i32.const 0))
```
Case 3: oversized initial memory:
```wat
(memory (export "memory") 100)
```
Run:
```
bazel build @wasm_repro//:wasm_repro
```
Full harness attached (`wasm-execute-poc-harness.tar.gz`) — precompiled `.wasm` included alongside `.wat` sources so `wabt` isn't required to run it.:
### Which operating system are you running Bazel on?
Linux
### What is the output of `bazel info release`?
release 9.2.0
### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.
N/A
### What's the output of `git remote get-url origin; git rev-parse HEAD` ?
```text
N/A: issue is against `bazelbuild/bazel` itself, not a downstream project.
```
### If this is a regression, please try to identify the Bazel commit where the bug was introduced with bazelisk --bisect.
Not a regression, all three trace to the initial implementation commit `6b826a2888` (2026-05-15), which itself ships the `#26092` TODOs describing this exact unfinished validation.
### Have you found anything relevant by searching the web?
No public advisories against the Chicory WASM runtime (`com.dylibso.chicory`, currently at 1.7.5) itself, its core interpreter/compiler is under continuous fuzzing via a "Nightly Fuzz" CI workflow in that project. These three findings are specifically in Bazel's own integration glue around Chicory (`StarlarkWasmModule.java`), not in Chicory's core engine, consistent with the glue code being newer and outside that fuzzing coverage.
### Any other information, logs, or outputs that you want to share?
Verified reproducible on a fresh Bazel 9.2.0 install, not just the original test environment:
```
=== Control (correct signature) ===
run -> return_code=0 output="AAAA" error=""
=== Test 1: mismatched signature ===
run -> return_code=8 output="" error=""
=== Test 2a: OOB pointer ===
run_oob_ptr -> return_code=-1 output="" error="Error executing run_oob_ptr: out of bounds memory access: attempted to access address: 1000000 but limit is: 65536 and size: 16"
=== Test 2b: overflow length ===
run_overflow_len -> return_code=-1 output="" error="Error executing run_overflow_len: Requested array size exceeds VM limit"
=== Test 3: oversized initial memory ===
run -> return_code=0 output="\x00\x00\x00\x00" error=""
```
Happy to send a PR for any of these if maintainers want a specific direction, surgical fixes: (1) validate the exported function's `FunctionType` against the expected signature before calling it in `run()`, (2) check `outputLen` against `memory_limit` (or actual instance memory size) before allocating the read buffer, and (3) throw instead of silently clamping when declared initial memory exceeds `memory_limit`, per the TODO's own suggestion. Open to other approaches maintainers prefer.
Contributor guide
Research direction
Start in StarlarkWasmModule.java at the TODOs around lines 239 and 317, tracing repository_ctx.load_wasm() and execute_wasm() through the run path. Use the supplied MODULE.bazel, repro.bzl, and .wat cases to reproduce each behavior; done means the three unfinished validations have clear, tested outcomes without changing the existing out-of-bounds handling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, wasm
- Domain
- build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100