Repository navigation
feat(wasm-sandbox): community parser isolation boundary (Closes #405) - #455
Open
woahwhattheheck wants to merge 22 commits into
Open
woahwhattheheck wants to merge 22 commits into
woahwhattheheck wants to merge 22 commits into
Conversation
…oundation#405) Add a real untrusted-code execution boundary for community-contributed parsers: threat model, WasmSandboxRunner with host-capped env.memory and worker-enforced CPU timeout, adversarial fixtures (loop/OOM/OOB/WASI/fs/ malformed output), Translation Registry integration with parserProvenance=community-wasm, authoring guide + Rust example, and npm run test:wasm. Closes Open-audit-foundation#405
Copy the sandbox's CJS worker into both runtime images at the path used by the compiled runner. TypeScript does not emit this asset, so a valid parser previously failed with Cannot find module in the production layout. Verified the compiled runner executes the same committed parser from both corrected runtime layouts. The existing WASM suite passes all 17 tests.
Persist parser provenance and sandbox failures, preserve them during replay, and prefer authoritative stored translations over stale cache entries. Initial dashboard loads and contract searches now share a metadata adapter and retain stored community outcomes. Show the community marker in the feed and details, including the failure code. Add two nullable Event columns without inferring legacy provenance. Focused registry, persistence, dashboard and resolver checks pass 122 cases; Prisma schema validation, client generation and the two-column migration diff pass. Preserve the existing memory-limit and Docker worker-asset repairs.
Reuse the nine production build-repair postimages from 31972c3. The original blobs matched that repair's parent, preserving this branch's newer sandbox, Docker asset and persisted-provenance changes. Remove the shared translation registry's runtime import of the Node-only community registry. Community registration supplies a typed async callback that delegates to the existing isolated worker. Native browser translation and server sandbox outputs retain their existing paths. Validation on this exact twelve-file composition: npm run build passes with Node heap 512 MiB (Turbopack, TypeScript, all 22 static pages); npm run lint passes. The committed community manifest executed through the real worker and returned translated transfer/community-wasm, while synchronous translation stayed cryptic. This single execution is not a performance benchmark. No optional test sweep or full-suite success is asserted. Original prerequisite repair: 31972c3 Operation: OA455-456-BUILD-REPAIR-REUSE-20261003-01
Carry persisted parser origin and sandbox failure codes through streamed CSV, JSON and NDJSON, plus the existing small browser download paths. Preserve unknown legacy metadata as JSON null and blank CSV fields. Extend the existing export regression suite through the actual dialog and download Blob. All 15 focused checks pass on the composed source. Preserves the prerequisite and client boundary repair from fa32226.
Carry persisted parserProvenance and sandboxError into the signed webhook payload, with explicit nulls for legacy events. Cover community success/failure, native output, and legacy metadata through actual delivery projection and retry in the existing webhook integration suite.
Decode complete numeric ScVal payloads through the Stellar codec instead of reading the type header as an amount. Preserve signed and unsigned 32/64/128-bit values, reject malformed/non-numeric input to the existing zero fallback, and round the seven-decimal base units using bigint arithmetic. Update the developer example to valid SDK-produced I128 XDR and exercise normalization plus custom-ABI translation in the existing amount suite. Leave missing token-registry and release helper APIs outside this repair.
Serialize input once before loading module bytes and reuse that exact JSON for the worker. This removes duplicate serialization and prevents mutation or stateful toJSON from replacing the payload after the input-size check. Add focused regressions for single serialization and loading-time mutation.
Check defined table descriptors before allocation or guest start: require explicit maxima, cap their aggregate at 65,536 reference slots and permit at most 32 tables. Preserve ordinary bounded funcref/externref tables and reject unsupported encodings instead of misreading their limits. Add the same 14 real-worker adversarial and compatibility cases to the existing Vitest suite and a dependency-free Node entrypoint. On Node 22.16.0, unchanged worker from 8ad1028: 6 pass / 8 fail; repaired worker: 14 pass / 0 fail. Syntax checks pass. No runtime mocks, dependency install, full application/registry/Vitest or compiled Rust-example run is claimed. Update the existing authoring and architecture documents with the table ABI, reproduction and precise resource-boundary limitations. Linear-memory/table caps are not a total-process RSS or global OOM guarantee. Rejoin cc39ac3 and preserve its independent input-snapshot runner fix and regressions unchanged. Only the five table-scope files change in this successor. Original PR and all prior contributions remain intact. [skip ci]
A parser may return output inside its input allocation. Calling optional dealloc before get_output_len and UTF-8 decoding lets guest cleanup overwrite an otherwise valid result. Capture the bounded output string first, then perform best-effort cleanup in finally. Preserve cleanup on output-boundary failures, ignored cleanup traps and the existing translate-trap path. Add one maintained runner regression with a 223-byte WASM fixture and matching WAT: translate writes a 63-byte JSON result in place, dealloc poisons it and grows memory from one page to two. Exact output and 131072-byte memory assertions prove output preservation and retained cleanup. Document that ABI lifetime in the existing authoring guide. Validation: Node 24.19.0, real production runner/Worker/WebAssembly. Four focused scenarios (in-place output, cleanup trap, malformed JSON, output cap) change from 2/4 correct on worker b18aa72 to 4/4 correct. Maintained runner.test.ts passes all 9 tests on retained Vitest 5.0.1 in 616ms, using the unchanged wasm test config with only scratch cache/one-worker settings. Worker syntax check passes. No new dependencies; no full application, registry, typecheck or CI result asserted. Preserve the prior input snapshot, memory and table-boundary repairs and the original PR455 contribution.
The manifest promises its version in schemaVersion, but registration dropped that value and both adapter outcomes returned null. A parser declared as 2.3.4 was therefore registered as 1.0.0 and produced an unversioned result. Copy the optional manifest label into the community blueprint, carry it through the existing registry, and include it on successful and failed sandbox results. Unversioned blueprints retain an absent version property and null translated schemaVersion. Schema selection, ledger windows, caching and native blueprint behavior are unchanged. Extend the existing registry-integration tests for file-manifest versions, nondefault 2.3.4 selection/translation, failed versioned execution, and the unversioned null controls. Focused Node 24.19.0 execution of the actual registration, resolver, runner and worker with the committed echo WASM passes 5/5 scenarios; baseline fb9145b passed only the two unversioned controls (2/5). File and byte registration, success and worker compilation failure preserve status, event type, provenance and error. This commit follows b8cc88a and preserves its concurrent output-lifetime repair. The same five cases also pass against its actual worker blob 0514342. The focused VM probe stubs unrelated native factories, metrics, localization, decoders and sanitizer. The maintained Vitest integration file, full application build and full suite were not run here; no dependency installation or CI rerun is claimed.
Interpret the signed i32 translate result as an unsigned wasm32 address before the existing output-range check. Reject values outside signed i32 representation before coercion, so a malformed f64 return cannot wrap into a valid offset. Preserve output copying before guest deallocation. The parent worker accepts -128 as a Buffer tail offset even though the wasm32 address is 4294967168 and the memory has only 65536 bytes. A valid 173-byte module reproduced that result. Ordinary and true tail addresses both returned the expected output. This concerns guest-memory range validation; no host-memory disclosure or sandbox escape is claimed. Add two maintained adversarial cases with 174/179-byte valid WASM fixtures and their WAT sources, and document unsigned pointer interpretation. Validation on parent f047dbf plus these source changes: Node 24.19.0; existing runner.test.ts and adversarial.test.ts, 19 passed in 2.10 seconds. Command: vitest run --config vitest.wasm.config.ts lib/wasm-sandbox/__tests__/adversarial.test.ts lib/wasm-sandbox/__tests__/runner.test.ts --maxWorkers=1 --no-file-parallelism --no-cache Used existing installed Vitest 4.1.10 read-only; no dependency installation or manifest/lock change. Declared project Vitest is ^3.2.7, so this is not an exact locked-dependency or full-application/registry/typecheck CI pass. node --check lib/wasm-sandbox/worker.cjs passed. Seven files only; original PR Open-audit-foundation#455 and all prior source repairs preserved.
Check the original get_output_len result is an integer within the signed i32 range before positive-length, output-cap and memory-range validation. Return INVALID_OUTPUT for malformed values instead of wrapping a claimed length into a small valid JSON slice. A valid 165-byte module returning f64 4294967335 previously read 39 bytes and succeeded. Actual Worker replay now rejects that value, fractional 39.5 and negative -4294967257; ordinary i32 length 39 remains successful. The maintained adversarial test includes the oversized-length regression with committed WAT and WASM fixtures. Existing adversarial.test.ts: 11/11 pass, zero failures or skipped cases, Node 24.19.0 and retained Vitest 4.1.10. The declared range is ^3.2.7. The source-bound command, preliminary baseline timeout and validation limits are recorded in COMMUNITY_PARSER_GUIDE.md. No dependency or production timeout changes; no full-app, registry or hosted CI claim. Preserve the preceding unsigned-pointer and output-lifetime repairs and the original PR Open-audit-foundation#455 branch and contribution history.
Add target-specific --import-memory linking and document the existing build script's required working directory. The original Rust guest compiled but declared private memory, which the sandbox correctly rejects. The repaired module imports only env.memory and retains the host memory ceiling. Actual Rust/Cargo 1.98.1 paired builds and the unchanged real worker passed in run 37201229337. Original 36966-byte module: FORBIDDEN_IMPORTS. Repaired 37009-byte module: expected sample transfer, 1179648 bytes reported linear memory. One-page host cap: MEMORY_LIMIT_EXCEEDED for 17 required initial pages. Replayed the same binaries against current composed worker 69df267 at parent 08de844; all three cases passed on Node 24.19.0. Preserve the concurrent output-length validation and earlier output-pointer repair. Only the example Cargo config and README change. Cloud controller and replay remain on an isolated validation branch. Exact build/source/run identities are documented in the README. This is compiled-guest compatibility evidence, not full-app, registry, real XDR or statistical performance evidence.
Author
|
I am claiming consideration for any GrantFox reward applicable to my contribution in this PR for #405, payable to @woahwhattheheck. Please confirm that this PR is associated with the original contributor's campaign record and included in the reward evaluation, and confirm any allocation and payout date once approved. Existing contributors' attribution and any remaining acceptance requirements remain documented in this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #405.
Builds a real untrusted-code execution boundary for community-contributed contract parsers (not a stub wrapper). Native TypeScript blueprints stay the reviewed path; community parsers run only through the sandbox and are labeled
parserProvenance: "community-wasm".What landed
lib/wasm-sandbox/WASM_SANDBOX_ARCHITECTURE.md(risk → mitigation → test mapping)WasmSandboxRunner— host-providedenv.memory(capped pages), worker_threads force-terminate for CPU budget, input/output size caps, typed errorsenv.memory; WASI /fs/ Node API imports rejected pre-instantiate (host + worker)registerCommunityParserFromManifest/FromBytes,translateEventAsync/translateWithCachepath, file-based example underlib/wasm-sandbox/community/COMMUNITY_PARSER_GUIDE.md,examples/rust/(wasm32-unknown-unknown)npm run test:wasm— vitest node env suiteAcceptance checklist
npm run test:wasmruns the suite aboveHonest gaps / follow-ups
descriptiontext is still attacker-chosen copy (sanitized, labeled community-sourced).Test plan
Observed locally: 13/13 passed (runner + adversarial + registry integration).