Skip to content

feat(parser): wasm parser-module loader, hardening, and authoring target (0.24.0) - #173

Open
facontidavide wants to merge 2 commits into
mainfrom
feat/parser-extensibility-1b
Open

feat(parser): wasm parser-module loader, hardening, and authoring target (0.24.0)#173
facontidavide wants to merge 2 commits into
mainfrom
feat/parser-extensibility-1b

Conversation

@facontidavide

@facontidavide facontidavide commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

SDK PR 1b of the parser-extensibility v4 architecture (spec, PlotJuggler/pj-official-plugins#272) — follows #172 (0.22.0, merged). A second loader for the already-frozen ABI: wasmer execution, hardening, budgets, and the wasm authoring target. Ships as 0.24.0 (0.23.0 went to the symbol-provenance fix, #175).

Rebased onto main (2026-08-30) after #174#176: the budget-aware NativeParserModule::load now keeps #175's defining-object symbol provenance (recorded_path), the CHANGELOG gained a 0.24.0 entry above 0.23.0, and root CLAUDE.md / the plugin skill's parser-module.md / kit README / ARCHITECTURE.md no longer describe wasm as unavailable. 80/80 Debug+ASAN with wasi-sdk 27 + wasmer 7.0.1.

Review rework (2026-08-30, commit 7e3b64d) — after a simplicity-first review (Claude + Codex, findings cross-verified):

  • The wasm instance wrapper now has the native wrapper's fault contract: it classifies, the host records strikes / quarantines / replays through the shared ParserModuleStrikeTracker. Removes the per-instance self-recreation (claim-wide state, per-instance replay) and the loader-owned tracker that concurrent instances raced on.
  • ParserModuleStrikeTracker and ParserModuleSessionBudgetTracker are thread-safe; a fault during replay now disables instead of leaving a claim quarantined forever.
  • Budgets count resources under opaque reservation ids, not manifest ids (native+wasm of one source, or two generations, no longer block each other). No process-global default budget; NativeParserModule::load(path, sink) behaves exactly as in 0.22.
  • WasmParserModule::load(path, options) — one signature instead of four overloads.
  • Table section is now inspected and capped (default 65536 elements) by the single pj_base audit shared with pj-wasm-embed-manifest.
  • Singlepass backend when available (linear compile time on untrusted input); docs state metering is an instruction budget, not a wall-clock deadline.
  • Executor is in-tree only (BUILD_INTERFACE, not installed); the never-taken PJ_SDK_WITH_WASMER config block is gone. Installed/Conan packages stay wasmer-free and ship the authoring preset + tool.
  • Tests: token-zero path really reaches the guest, table caps, host-driven quarantine loop, concurrent instances, concurrent budget accounting. 80/80 Debug+ASAN.

The pre-existing Conan plugin_host component gap (missing pj_parser_module_host / pj_parser_claim_catalog since 0.22) is fixed separately on main.

Exit-criterion prototype findings (spec §13 gate)

  • The pinned libwasmer.a (7.0.1, static) declares but does not export wasm_module_share/obtain — the loader uses engine-owned module reuse: one compilation, store-per-bound-instance, isolated state verified.
  • No creator-thread affinity observed; calls are sequential-only, host-serialized (documented in the loader header — per-store executors remain PR-2 host work).
  • The lib exports the full wasmer_metering_* C API but no interrupt/epoch/deadline surface — execution limits are enforceable instruction metering (fresh allowance per guest call), not a wall-clock watchdog that couldn't fire.

Surface

Area Contents
Loader Validation before instantiation: manifest section (shared codec, exactly one), reactor enforced (_initialize; start/_start rejected), export names and signatures audited via the shared pj_base wasm inspector, empty frozen import allow-list (a parser module may import nothing)
Runtime Metered store-per-instance execution; linear-memory base re-acquired at every point of use, overflow-safe bounds; splices resolve against the original host payload; shared fault taxonomy + strike tracker with quarantine replay
Caps & budgets Artifacts must declare a linear-memory maximum (default cap 256 MiB, engine-enforced at runtime); session budgets (modules / artifact size / claims / instances / aggregate declared memory) gate admission with DECLINE, no mutation on rejection
Adversarial fixtures Unreachable trap, metered infinite loop, memory-growth bomb, admission limits, quarantine replay → repeat-disable, plus the load-rejection matrix
Authoring pj-wasm-embed-manifest installed CLI (embed/verify); pj_add_parser_module(... TARGETS native wasm) builds both artifacts from one source with a post-link audit — dogfooded on the toy module
Packaging wasmer required only by the plugin_host component; plugin_sdk consumers stay wasmer-free. PJ_WASMER_ROOT gate with graceful skip; CI installs wasmer 7.0.1 and runs the wasmer-labeled tests

Verification

  • 80/80 Debug+ASAN with both toolchain roots (wasi-sdk 27 + wasmer 7.0.1); graceful-skip verified for each root independently
  • Installed-package consumer produced native + wasm artifacts from one source
  • Process: milestone reviews with independent re-verification (1b-M1, 1b-M2), a simplification pass, and one error-path leak fix (metering middleware adapter failure) before squashing

🤖 Generated with Claude Code

@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 8dfaa32 to 6bc55f6 Compare August 9, 2026 09:11
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1a branch from ad01f03 to babd073 Compare August 9, 2026 09:18
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 6bc55f6 to d02344d Compare August 9, 2026 09:21
@facontidavide facontidavide changed the title feat(parser): wasm parser-module loader, hardening, and authoring target (0.23.0) feat(parser): wasm parser-module loader, hardening, and authoring target Aug 9, 2026
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch 2 times, most recently from 0279ed5 to 21f11b7 Compare August 9, 2026 09:42
@facontidavide
facontidavide marked this pull request as draft August 9, 2026 10:05
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1a branch from babd073 to 4e42f8b Compare August 9, 2026 11:03
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 21f11b7 to 93931f0 Compare August 9, 2026 11:03
@facontidavide
facontidavide marked this pull request as ready for review August 9, 2026 11:03
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1a branch from 4e42f8b to 08503f7 Compare August 9, 2026 11:34
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 93931f0 to f977260 Compare August 9, 2026 11:34
@facontidavide
facontidavide marked this pull request as draft August 9, 2026 14:28
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from f977260 to 878cb93 Compare August 9, 2026 14:28
@facontidavide facontidavide changed the title feat(parser): wasm parser-module loader, hardening, and authoring target feat(parser): wasm parser-module loader, hardening, and authoring target (0.23.0) — postponed Aug 9, 2026
@facontidavide

Copy link
Copy Markdown
Contributor Author

Postponed — the core PR (#172) merges first on its own as 0.22.0.

This was the explicit rationale for splitting the SDK work in two: 1b is a second loader for an ABI that #172 already freezes, so deferring wasm delays wasm, never the architecture. Nothing here blocks #172, and #172 ships a complete native module story on its own.

State of this branch while it waits:

  • VERSION restored to 0.23.0 with its own CHANGELOG section (the single-bump arrangement only made sense when the two merged back to back).
  • Rebased on the current feat/parser-extensibility-1a; 80/80 with both toolchains (wasi-sdk 27 + wasmer 7.0.1), all review findings fixed.
  • After feat(parser): extensible parser routing + functional parser modules (0.22.0) #172 merges this needs a rebase onto main and a retarget; then it is ready for review as-is.

🤖 Generated with Claude Code

@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1a branch from 08503f7 to 3501581 Compare August 9, 2026 14:41
@facontidavide

Copy link
Copy Markdown
Contributor Author

Note for revival: feat/parser-extensibility-1a has since been amended (docs + skills synchronized, VERSION made a CMake configure dependency), so this branch now trails it.

When picking this back up, after #172 merges: rebase onto main, then expect doc conflicts in parser_module/README.md, pj_plugins/CLAUDE.md, and pj_plugins/docs/ARCHITECTURE.md#172 rewrote those to describe the native-only 0.22.0 surface, so this PR's wasm sections need re-applying on top of that new baseline rather than merged blindly. The wasm authoring reference (.claude/skills/plotjuggler-plugin/references/parser-module.md) also currently documents native-only targets and will need its wasm section.

Deliberately not rebased now to avoid resolving those conflicts twice.

🤖 Generated with Claude Code

@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1a branch from 3501581 to 817130e Compare August 9, 2026 14:49
Base automatically changed from feat/parser-extensibility-1a to main August 9, 2026 14:57
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 878cb93 to 8a315d4 Compare August 30, 2026 17:18
@facontidavide facontidavide changed the title feat(parser): wasm parser-module loader, hardening, and authoring target (0.23.0) — postponed feat(parser): wasm parser-module loader, hardening, and authoring target (0.24.0) — postponed Aug 30, 2026
facontidavide added a commit that referenced this pull request Aug 30, 2026
…le caps, in-tree executor

Review rework of the wasm parser-module loader (PR #173). No ABI or protocol
change; the installed host C++ API of 0.22/0.23 is unchanged except for
additions, and NativeParserModule::load(path, sink) behaves exactly as in 0.22.

- The wasm instance wrapper now has the native wrapper's fault contract: it
  classifies traps, metering exhaustion, malformed descriptors, and bad splices
  as contract violations and returns them. Strike recording, quarantine,
  create/bind replay, and disabling are host policy through the shared
  ParserModuleStrikeTracker, exactly as for native modules. This deletes the
  per-instance self-recreation (which was claim-wide in state but per-instance
  in replay, and left sibling instances on poisoned stores) and the loader-owned
  tracker every instance of a module raced on.
- ParserModuleStrikeTracker is thread-safe (scalar and object routes of one
  claim run on different threads) and a contract violation while quarantined —
  the replay itself faulting — disables the claim instead of leaving it
  quarantined forever.
- ParserModuleSessionBudgetTracker counts resources under opaque reservation
  ids instead of manifest ids: two loads of one artifact, or native and wasm
  builds of one source, are two reservations; duplicate-provider policy stays
  in the catalog. It is thread-safe because wrappers release reservations from
  destructors on arbitrary threads. The process-global default budget is gone:
  both loaders take an optional budget and do no accounting without one.
- WasmParserModule::load has one signature (path + WasmParserModuleLoadOptions)
  instead of four overloads.
- pj_base validateParserModuleWasmArtifact() is the single static admission
  audit used by the loader and pj-wasm-embed-manifest. It now parses the table
  section: every table must declare a maximum and the aggregate is capped
  (default 65536 elements) — table storage is host memory outside the linear
  memory budget and is allocated at instantiation, before any metering.
- Compilation prefers Wasmer's Singlepass backend (linear compile time on
  untrusted input; no cancellable compile API exists). Docs now say metering
  is an instruction budget, not a wall-clock deadline.
- The executor is an in-tree component: pj_plugin_host links it through
  BUILD_INTERFACE only, it is not installed or exported, and the never-taken
  PJ_SDK_WITH_WASMER block in plotjuggler_sdkConfig.cmake.in is removed.
  Installed packages stay wasmer-free and ship the authoring preset and tool.
- Tests: token-zero creation error now reaches the guest (three-claim manifest
  on a two-claim module); table-cap loader rejections; host-driven quarantine
  loop; concurrent instances under one tracker; concurrent budget accounting;
  tracker disables on replay fault.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017U391nLF7Motf4FehVRiXz
@facontidavide facontidavide changed the title feat(parser): wasm parser-module loader, hardening, and authoring target (0.24.0) — postponed feat(parser): wasm parser-module loader, hardening, and authoring target (0.24.0) Aug 30, 2026
@facontidavide
facontidavide marked this pull request as ready for review August 30, 2026 18:27
facontidavide and others added 2 commits August 30, 2026 20:28
…get (0.24.0)

SDK PR 1b of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272), following the core PR (#172, 0.22.0): a second loader for the
already-frozen module ABI — wasmer execution, hardening, and the wasm
authoring target. Ships as its own 0.24.0 release (0.23.0 was taken by the
symbol-provenance fix, #175), so an SDK with the wasm loader is
distinguishable from one without it. Rebased onto main after #174#176: the
budget-aware NativeParserModule::load keeps #175's defining-object symbol
provenance, and the docs no longer describe wasm as unavailable.

- wasmer 7.0.1 statically linked (pinned; required only by the plugin_host
  component — plugin_sdk consumers stay wasmer-free). Exit-criterion prototype
  findings encoded in the loader contract: the pinned static lib exports no
  wasm_module_share/obtain symbols (engine-owned module reuse: one compilation,
  store-per-bound-instance with isolated state) and no creator-thread affinity
  exists — calls are sequential-only, host-serialized. Pin re-evaluated against
  7.2.1: no C-API gains, WASI-syscall CVEs unreachable under the empty import
  allow-list, and 7.2 drops x86_64-darwin (rationale in ARCHITECTURE.md)
- wasm loader: validation before any instantiation — manifest custom section
  via the shared codec (exactly one), reactor model enforced (_initialize
  required, start section/_start rejected), operational export set verified
  by name AND signature through the shared pj_base wasm inspector, and a
  frozen EMPTY import allow-list (a parser module may import nothing)
- execution runtime: metered store-per-instance calls (the pinned lib exports
  the wasmer_metering_* C API but no interrupt/epoch/deadline surface, so
  limits are enforceable instruction metering — fresh point allowance per
  guest call, exhaustion = distinct contract violation), linear-memory base
  re-acquired at every point of use with overflow-safe bounds, splices
  resolved against the original host payload, shared fault taxonomy + strike
  tracker with quarantine replay
- memory caps at validation: artifacts must declare a linear-memory maximum
  (default cap 256 MiB); the engine enforces it at runtime. Aggregate session
  budgets (modules, artifact size, claims, active instances, declared memory)
  gate admission with DECLINE and mutate nothing on rejection
- adversarial fixtures: unreachable trap, metered infinite loop, memory-growth
  bomb, admission limits, quarantine replay — plus the M1 rejection matrix
- pj-wasm-embed-manifest installed CLI (embed/verify) wrapping the shared
  section codec; pj_add_parser_module(... TARGETS native wasm) builds both
  artifacts from one source with post-link audit, dogfooded on the toy module
- fix: release the metering middleware on the adapter-failure path
- VERSION 0.24.0, CHANGELOG entry, CI wasmer job (metering-symbol check)

Tests: 80/80 Debug+ASAN with both toolchain roots; graceful skip verified for
each root independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_017U391nLF7Motf4FehVRiXz
…le caps, in-tree executor

Review rework of the wasm parser-module loader (PR #173). No ABI or protocol
change; the installed host C++ API of 0.22/0.23 is unchanged except for
additions, and NativeParserModule::load(path, sink) behaves exactly as in 0.22.

- The wasm instance wrapper now has the native wrapper's fault contract: it
  classifies traps, metering exhaustion, malformed descriptors, and bad splices
  as contract violations and returns them. Strike recording, quarantine,
  create/bind replay, and disabling are host policy through the shared
  ParserModuleStrikeTracker, exactly as for native modules. This deletes the
  per-instance self-recreation (which was claim-wide in state but per-instance
  in replay, and left sibling instances on poisoned stores) and the loader-owned
  tracker every instance of a module raced on.
- ParserModuleStrikeTracker is thread-safe (scalar and object routes of one
  claim run on different threads) and a contract violation while quarantined —
  the replay itself faulting — disables the claim instead of leaving it
  quarantined forever.
- ParserModuleSessionBudgetTracker counts resources under opaque reservation
  ids instead of manifest ids: two loads of one artifact, or native and wasm
  builds of one source, are two reservations; duplicate-provider policy stays
  in the catalog. It is thread-safe because wrappers release reservations from
  destructors on arbitrary threads. The process-global default budget is gone:
  both loaders take an optional budget and do no accounting without one.
- WasmParserModule::load has one signature (path + WasmParserModuleLoadOptions)
  instead of four overloads.
- pj_base validateParserModuleWasmArtifact() is the single static admission
  audit used by the loader and pj-wasm-embed-manifest. It now parses the table
  section: every table must declare a maximum and the aggregate is capped
  (default 65536 elements) — table storage is host memory outside the linear
  memory budget and is allocated at instantiation, before any metering.
- Compilation prefers Wasmer's Singlepass backend (linear compile time on
  untrusted input; no cancellable compile API exists). Docs now say metering
  is an instruction budget, not a wall-clock deadline.
- The executor is an in-tree component: pj_plugin_host links it through
  BUILD_INTERFACE only, it is not installed or exported, and the never-taken
  PJ_SDK_WITH_WASMER block in plotjuggler_sdkConfig.cmake.in is removed.
  Installed packages stay wasmer-free and ship the authoring preset and tool.
- Tests: token-zero creation error now reaches the guest (three-claim manifest
  on a two-claim module); table-cap loader rejections; host-driven quarantine
  loop; concurrent instances under one tracker; concurrent budget accounting;
  tracker disables on replay fault.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017U391nLF7Motf4FehVRiXz
@facontidavide
facontidavide force-pushed the feat/parser-extensibility-1b branch from 7e3b64d to 8873b0d Compare August 30, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant