Skip to content

wasm: upgrade contrib wasmtime to 47.0.4, audit the tarball pin, and test the runtime - #418

Open
andypost wants to merge 5 commits into
masterfrom
wasm/contrib-wasmtime-47
Open

andypost wants to merge 5 commits into
masterfrom
wasm/contrib-wasmtime-47

Conversation

@andypost

Copy link
Copy Markdown

Closes #400.

The contrib wasmtime pin was 43.0.1, four majors behind the Rust pin, and
nothing audited it: cargo audit reads Cargo.lock files only, and
pkg/contrib fetches a source tarball. This brings both pins to 47.0.4, adds an
OSV check keyed on the version file, and gives the legacy wasm module its
first runtime test.

Advisories

An OSV query on 43.0.1, deduplicated against aliases, returns six advisories.
47.0.4 returns none for either crate:

crate advisory issue
wasmtime RUSTSEC-2026-0114 (CVE-2026-44216) panic allocating a table larger than the host address space
wasmtime RUSTSEC-2026-0222 stores can mix type indices between engines
wasmtime RUSTSEC-2026-0269 (GHSA-vqjp-4c8c-hfgg) filesystem sandbox escape via trailing slashes
wasmtime-wasi RUSTSEC-2026-0149 (CVE-2026-47261) path_open(TRUNCATE) bypasses FilePerms::WRITE
wasmtime-wasi RUSTSEC-2026-0182 (CVE-2026-54786) fd_renumber leak
wasmtime-wasi RUSTSEC-2026-0188 (CVE-2026-58494) hard links and renames bypass destination FilePerms

#400 named only -0269 and -0222; the check added here found the other three
wasmtime-wasi path/permission advisories on its first run.

-0269 is reachable through this module: wasmtime_linker_define_wasi()
(src/wasm/nxt_rt_wasmtime.c:359) defines the WASI p1 surface including
path_open, and each access.filesystem entry is preopened read+write with the
host path used verbatim as the guest path (:283-291). The new test exercises
the sandbox boundary, including the symlink-with-trailing-slash shape.

Changes

  • Pin bump -- pkg/contrib/src/wasmtime/{version,SHA512SUMS}, and
    -DWASMTIME_FEATURE_WASI_HTTP=OFF in the c-api build. v47 defaults wasi-http
    on, which pulls the rustls stack into the linked libwasmtime.so for an API
    this module never uses. Source compatibility checked against the v47 headers:
    every symbol the module calls still exists, the only signature changes are
    added const, and the removed declarations are the unused externref /
    anyref GC helpers.
  • Coverage -- pkg/contrib/check-advisories.py and a pins job in
    Audit (cargo). It reads WASMTIME_VERSION, queries OSV for wasmtime and
    wasmtime-wasi, and refuses to pass unless pkg/eol.json and SHA512SUMS
    agree with the version file. The workflow's path filters cover the pin files,
    so a bump cannot skip it.
  • Runtime test -- test/test_wasm.py builds a libunit-wasm guest from
    pkg/contrib sources at test time and drives it through unitd: a fixed body,
    a file read inside an access.filesystem preopen, and a denied read outside
    it. The wasm build-test leg now installs pytest and the guest toolchain, uses
    --rpath so pytest can dlopen the module, runs unprivileged so the guest
    build stays runner-owned, and fails if no smoke test passed instead of going
    green on a skip. Timeout 30 -> 45 minutes because the first run on a bumped
    pin is a cold wasmtime build.
  • EOL -- EOL.md and pkg/eol.json name both pins at 47.0.4, each covered
    by the check that can reach it.

Verification

  • python3 pkg/contrib/check-advisories.py exits 1 on 43.0.1 listing the six
    advisories above, and 0 on 47.0.4.
  • make -C pkg/contrib .wasmtime builds the c-api from source with the feature
    off; ./configure wasm --rpath ... && make wasm compiles without warnings.
  • pytest test/test_wasm.py -> 3 passed.
  • Mirror: Wasmtime 47.0.4 packages.freeunitorg.github.io#10 merged and serving;
    packages.freeunit.org/wasmtime/wasmtime-v47.0.4-src.tar.gz returns 200 at
    199,762,846 bytes with SHA-512 3a27dada...b078, matching the committed
    SHA512SUMS.

Notes

  • No CHANGES/docs/changes.xml entry: the release process generates those.
  • The pkg/deb/pkg/rpm wasm Makefiles use a Rust 1.88 toolchain that already
    could not build 43.x (1.91 floor); no CI target builds wasm through them, so
    they are untouched.

Checklist

  • I have read CONTRIBUTING.md
  • If applicable, I have added tests
  • If applicable, I have updated documentation

43.0.1 is affected by six distinct advisories: wasmtime RUSTSEC-2026-0114 (CVE-2026-44216, table allocation panic), -0222 (cross-engine type indices) and -0269 (filesystem sandbox escape via trailing slashes), plus wasmtime-wasi RUSTSEC-2026-0149 (path_open TRUNCATE bypasses FilePerms::WRITE), -0182 (fd_renumber leak) and -0188 (hard links and renames bypass destination FilePerms). 47.0.4 returns none for both crates.

RUSTSEC-2026-0269 is reachable through this module: wasmtime_linker_define_wasi() defines the WASI p1 path surface and every access.filesystem entry is preopened read+write with the host path used verbatim as the guest path.

The c-api build turns WASMTIME_FEATURE_WASI_HTTP off; the wasm module only uses p1 WASI and v47 defaults wasi-http on, which pulls the rustls stack into the linked library. Source compatibility checked against the v47 headers: every symbol the module calls still exists; the only signature changes are added const, and the removed declarations are the unused externref/anyref GC helpers.

The tarball comes from the mirror: freeunitorg/packages.freeunitorg.github.io#10. EOL.md and pkg/eol.json now name both pins at 47.0.4.
cargo audit reads Cargo.lock files only, so the wasmtime source tarball that pkg/contrib fetches has never had an automated advisory check. check-advisories.py reads WASMTIME_VERSION, queries OSV for wasmtime and wasmtime-wasi at that version, and refuses to trust the pin unless pkg/eol.json and SHA512SUMS agree with the version file.

Run against 43.0.1 it reports the six advisories the bump closes, including three wasmtime-wasi path/permission issues that #400 did not name; 47.0.4 is clean. The new pins job joins the existing weekly schedule, and the path filters cover pkg/contrib/src/*/version and SHA512SUMS so a pin bump cannot skip it.
The legacy wasm module had compile-only CI and no runtime test at all. test/test_wasm.py builds test/wasm/hello/hello.c at test time against pkg/contrib's wasi-sysroot and libunit-wasm source, then drives it through unitd: a fixed body, a file read inside an access.filesystem preopen, and a denied read outside it including a symlink with the trailing slash RUSTSEC-2026-0269 abused.

The wasm build-test leg now installs pytest and the guest toolchain, configures with --rpath so pytest can dlopen the module, runs unprivileged so the guest build is runner-owned, and fails if no test_wasm.py test passed instead of going green on a skip. Its timeout grows to 45 minutes because the first run on a bumped pin rebuilds wasmtime from source.

Local: pytest test/test_wasm.py -> 3 passed against 47.0.4.
@andypost andypost added z-bug 🐞 Bug report z-tests z-wasm WebAssembly language module labels Sep 18, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cbe0d591a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/unit/applications/lang/wasm.py Outdated
Comment thread .github/workflows/build-test.yml Outdated
Review follow-up. On Ubuntu, clang -dumpversion prints the full release (21.1.8 here, 20.1.8 in the wasi-sdk), so the old command asked apt for a libclang-rt-20.1.8-dev-wasm32 that does not exist and the wasm leg died before the tests. Take the major component with cut -d. -f1.

The linker flags were already a single -Wl, argument: adjacent string literals concatenate, so clang received -Wl,...,--stack-first,-z,stack-size=8388608 and passed -z stack-size=8388608 to the linker. Verified both ways with system clang 21 (the joined form is accepted, only the standalone -z,stack-size=... is an unknown argument), but the implicit split invited the misread, so build the flag explicitly now. pytest test/test_wasm.py still 3 passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9d6a26439

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/contrib/check-advisories.py
@andypost

Copy link
Copy Markdown
Author

@claude review

@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown

Claude finished @andypost's task in 6m 38s —— View job


Review — PR #418

  • Gather context (diff vs origin/master, src/wasm, test harness)
  • Review the wasmtime pin bump + c-api build change
  • Review pkg/contrib/check-advisories.py
  • Review the test fixtures
  • Review CI workflow changes
  • Post review

The pin bump itself is solid and well argued: WASMTIME_FEATURE_WASI_HTTP=OFF is the right call for a module that only links the p1 surface, the WASMTIME_VERSION_MAJOR >= 27 guard at src/wasm/nxt_rt_wasmtime.c:283 already covers the v47 wasi_config_preopen_dir() signature, and src/wasm-wasi-component/Cargo.lock:1591 confirms both pins now read 47.0.4. Giving the legacy module its first runtime test is overdue.

A few things I'd want fixed before merge.

1. The app config never names the guest's init/end handlers

test/unit/applications/lang/wasm.py:77-84 builds the application object with request_handler, malloc_handler and free_handler, but not module_init_handler / module_end_handler. src/wasm/nxt_wasm.c:408-409 only wires those hooks when the config names them, and NXT_WASM_DO_HOOK(NXT_WASM_FH_MODULE_INIT) at nxt_wasm.c:365 is a no-op when func_name is NULL.

So test/wasm/hello/hello.c:23-33 exports luw_module_init_handler and luw_module_end_handler, and neither is ever called — request_buf is still NULL when luw_request_handler() reaches luw_set_req_buf(&ctx, &request_buf, LUW_SRB_NONE) at hello.c:117.

That the three tests pass anyway is the worrying part. In wasm32 linear memory address 0 is an ordinary writable address, so the copy luw_set_req_buf() performs lands at offset 0 rather than trapping; with --stack-first and an 8 MB stack that region is the deep end of the shadow stack, which nothing is using. The fixture works by accident, and the first guest that recurses meaningfully or switches to LUW_SRB_ALLOC will fail in a way that looks unrelated. Either add the two handlers to the config (what the upstream luw-echo-request config does) or drop them from hello.c and stop pretending the buffer is initialised.

Fix this →

2. A guest build failure produces no diagnostics anywhere

test/unit/applications/lang/wasm.py:67-70 runs clang with stderr=subprocess.STDOUT and then discards error.output entirely on CalledProcessError. Returning None is right for check_clang_wasm() — that's a feature probe — but load() at line 75 uses the same path and stringifies the result straight into the config: 'module': str(None)"module": "None". unitd then reports a missing file for a compile error.

The workflow's fallback is no help either: build-test.yml:787 greps the pytest log for SKIPPED|feature, which will not contain a single line of compiler output. A wasm leg that goes red because of a toolchain mismatch gives a maintainer nothing to work with. Stash the captured output on the class and have load() fail with it.

Fix this →

3. --rpath with a relative --lib-path gives a CWD-relative RUNPATH

.github/workflows/build-test.yml:643:

./configure wasm --rpath --include-path=pkg/contrib/wasmtime/artifacts/include --lib-path=pkg/contrib/wasmtime/artifacts/lib

Bare --rpath sets NXT_WASM_RPATH="", and auto/modules/wasm:82 then copies --lib-path into it verbatim, producing -Wl,-rpath,pkg/contrib/wasmtime/artifacts/lib. A relative DT_RUNPATH is resolved by ld.so against the process working directory, not the module's location, so libunit.so's wasm module only resolves libwasmtime.so while unitd is started from the repo root. It happens to hold here because pytest runs from the workspace root, but it silently breaks for anyone running the suite from test/, and it makes the built tree non-relocatable. --rpath=$(pwd)/pkg/contrib/wasmtime/artifacts/lib costs nothing and removes the coupling.

4. The pins job's path filter is much wider than what the script checks

audit.yml now triggers on pkg/contrib/src/*/version and pkg/contrib/src/*/SHA512SUMS, but check-advisories.py:23-30 hardcodes the wasmtime pin and the wasmtime / wasmtime-wasi crates. pkg/contrib/src/ also holds njs, wasi-sysroot and libunit-wasm. Bumping any of those fires the job, the job reports wasmtime, wasmtime-wasi 47.0.4: no advisories, and it goes green having checked nothing about the pin that actually changed — which is a worse outcome than not running at all, because the green check reads as coverage. Either narrow the filter to the wasmtime pin files, or make the script table-driven over the pins so adding one is a data change. Given the PR's own framing ("a bump cannot skip it"), I'd go for the table.

On Codex's transitive-graph point: agreed that it's a real gap, though a lower one — I'd note it in the docstring rather than block on it, since auditing the tarball's Cargo.lock means unpacking a 200 MB tarball in the pins job.

5. query() has no retry, so OSV flakiness reds the weekly gate

check-advisories.py:63-64 maps any URLError/timeout to fail() → exit 2. This is a scheduled gate; an OSV blip on a Monday morning is indistinguishable at the job level from a real problem, and the usual reflex (continue-on-error) would mask actual advisories. A two- or three-attempt retry with a short backoff inside query() keeps the signal meaningful.

6. test_wasm_access_filesystem_denied can pass vacuously

hello.c:60-63 returns 404 for any fopen() failure, so test/test_wasm.py:50 and :54 assert nothing stronger than "the guest could not open that path". A broken guest build, a wrong access.filesystem value, or a typo'd URL all yield the same 404. The positive case exists but lives in a separate test, so it does not protect this one.

Adding a positive control at the end of the same test — read data/hello.txt and assert 200 — makes the denial meaningful. While there, f'{data}/link' without the trailing slash is worth asserting too: that's the baseline sandbox behaviour RUSTSEC-2026-0269's trailing-slash variant sits on top of, and right now only the variant is covered.

Fix this →

Smaller things

  • check-advisories.py:47re.escape(tarball); the . in 47.0.4 and .tar.gz are currently wildcards. Harmless in practice, wrong in principle.
  • check-advisories.py:50fail() should be annotated -> NoReturn, otherwise read_version() and query() both have code paths a type checker reads as returning None.
  • The Fail if the wasm smoke test skipped grep depends on PASSED appearing in the log, which is only true because test/pytest.ini:2 sets addopts = -vvv. That coupling is load-bearing and invisible at the grep; one line of comment naming it would stop a future addopts cleanup from quietly disarming the guard.
  • build-test.yml:209 raises timeout-minutes to 45 for every test leg, not just wasm. ${{ matrix.build == 'wasm' && 45 || 30 }} keeps the tighter guard where the justification does not apply.
  • hello.c:81malloc(size) where size == 0 may return NULL and report a spurious 500; and there's no check that size fits the NXT_WASM_MEM_SIZE - RESPONSE_OFFSET window before luw_mem_writep_data(). Both are fine for the current fixtures but cheap to guard, and the second is the kind of thing the next person to add a test file will trip over.
  • hello.c:99send_file() relies on the default status rather than calling luw_http_set_response_status(LUW_HTTP_OK), unlike send_simple().
  • The consistency check covers pkg/eol.json but not EOL.md, so the markdown table can still drift from the pin. Since both were updated here, adding the table row to the check would keep them locked together.
  • make -C pkg/contrib .wasi-sysroot libunit-wasm only unpacks libunit-wasm; the .libunit-wasm target already builds the archive against the same sysroot. The test recompiles libunit-wasm.c on every load() plus once at discovery — four clang invocations a session — and duplicates upstream's build flags. Linking the archive would be both faster and closer to what users actually build against.

Verified while reviewing

  • test/pytest.ini sets -vvv, so the PASSED grep does match (this was worth checking — it would have been a silent job failure otherwise).
  • _clear_temp_dir() (test/conftest.py:741) wipes temp_dir between tests, so the bare data.mkdir() in both filesystem tests is safe.
  • discover_available() runs after unit_run() sets option.temp_dir, so check_clang_wasm() has a valid temp dir, and it short-circuits cheaply on non-wasm legs because pkg/contrib/wasi-sysroot is absent.
  • The clang -dumpversion | cut -d. -f1 fix in e9d6a26 is correct, and both -Wl, forms in wasm.py:36-47 are equivalent — the earlier adjacent-literal version was already one argument.

I could not run pkg/contrib/check-advisories.py or fetch the libunit-wasm 0.5.0 source from this environment (no network), so point 1's exact failure mechanism inside luw_set_req_buf() is inferred from the surrounding code rather than read; the missing config entries themselves are not in doubt.
· branch wasm/contrib-wasmtime-47

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-bug 🐞 Bug report z-tests z-wasm WebAssembly language module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wasm: the contrib wasmtime pin is four majors behind and audited by nothing

1 participant