Skip to content

Script injection via data sections#86

Merged
vigoo merged 3 commits into
mainfrom
script-injection
Mar 23, 2026
Merged

Script injection via data sections#86
vigoo merged 3 commits into
mainfrom
script-injection

Conversation

@vigoo

@vigoo vigoo commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Overview

BinarySlot is a dynamic JS injection mode for wasm-rquickjs that allows injecting JavaScript source into prebuilt WASM component templates without recompiling Rust. There is no capacity limit — the WASM component is structurally rewritten using wasmparser + wasm-encoder to accommodate any size JS payload. Multiple JS modules are supported: the primary export module and any number of additional modules can each use BinarySlot mode independently.

Marker Format (40 bytes)

Each BinarySlot module embeds a 40-byte marker file via include_bytes!. Layout:

[SLOT_MAGIC 16 bytes][MODULE_INDEX u32 LE][JS_OFFSET u32 LE][SLOT_END_MAGIC 16 bytes]
Field Offset Size Description
SLOT_MAGIC 0 16 b"WASM_RQJS_SLOT\x01\x00" — identifies a slot marker
MODULE_INDEX 16 4 Sequential u32: 0 = primary export module, 1+ = additional modules
JS_OFFSET 20 4 Pointer into linear memory. 0 = no JS injected yet
SLOT_END_MAGIC 24 16 b"WASM_RQJS_SLTND\x00" — footer for integrity validation

Constants: MARKER_SIZE = 40, WASM_PAGE_SIZE = 65536.

create_marker_file(module_index: u32) creates these markers with JS_OFFSET=0.

Template Compilation Flow

  1. Crate generation (copy_js_modules in lib.rs): For each BinarySlot module, a .slot file is created with a unique MODULE_INDEX (assigned sequentially across all BinarySlot modules). The primary export module gets index 0, additional modules get 1, 2, etc.

  2. Code generation (generate_module_defs in exports.rs):

    • If any module uses BinarySlot, a shared read_js_from_slot_bytes(slot: &[u8]) -> String function is emitted. This reads JS from the marker using volatile reads to prevent the optimizer from constant-folding the slot contents.
    • The primary BinarySlot module emits JS_EXPORT_MODULE_SLOT: &[u8] = include_bytes!("name.slot") and js_export_module() calls read_js_from_slot_bytes.
    • Each additional BinarySlot module emits its own include_bytes! static and a per-module reader function, used in the JS_ADDITIONAL_MODULES lazy vec.
  3. Compilation: cargo-component build compiles the crate to a WASM component. The 40-byte markers end up in data segments with JS_OFFSET=0.

Injection Flow

inject_js_into_component(input, output, js_sources: &[&str]) — the js_sources slice maps by position to MODULE_INDEX (first entry → MODULE_INDEX=0, second → MODULE_INDEX=1, etc.).

Reencode Pass (MarkerRewriter)

The MarkerRewriter struct implements wasm_encoder::reencode::Reencode and ReencodeComponent:

  1. parse_data: For each data segment:

    • Tracks max_data_end (highest memory address used by active segments on memory 0).
    • Scans segment bytes for markers using is_marker_at() (checks SLOT_MAGIC at offset 0 and SLOT_END_MAGIC at offset 24).
    • If a marker is found, reads its MODULE_INDEX and records it. Duplicate indices are an error.
    • Emits the segment unchanged (JS_OFFSET stays 0).
  2. parse_data_section: After processing all existing segments:

    • For each found marker (sorted by module index), appends a new active data segment on memory 0 at page_align(current_offset) containing [JS_LEN u32 LE][JS bytes].
    • Records (module_index, mem_offset) pairs in js_mem_offsets.
    • Each payload starts page-aligned after the previous one.
  3. data_count: Returns count + N where N is the number of JS payloads (one per BinarySlot module).

  4. parse_memory_section: Grows the core module's memory minimum pages to fit all JS payloads. Upper bound: original_min_pages * PAGE_SIZE + total_payload_size + N * PAGE_SIZE (worst-case page alignment padding per payload).

Post-Pass Binary Patch

patch_js_offsets_in_output(output, offsets):

  • Scans the final output bytes for all markers with JS_OFFSET == 0.
  • For each (module_index, js_mem_offset) pair, finds the marker with matching MODULE_INDEX and writes js_mem_offset into the JS_OFFSET field at byte offset 20.

Validation

After the reencode pass:

  • If no markers were found → error ("No JS injection markers found").
  • For each expected module index (0..N), verifies a marker was found → error if missing.

Runtime Reading

The generated read_js_from_slot_bytes(slot: &[u8]) -> String function:

  1. Validates SLOT_MAGIC at offset 0 via volatile reads.
  2. Reads JS_OFFSET at offset 20 (skipping MODULE_INDEX at offset 16) via volatile reads.
  3. Asserts JS_OFFSET > 0 (panics if slot is empty — JS not injected).
  4. Validates SLOT_END_MAGIC at offset 24.
  5. Reads JS_LEN (4 bytes) from linear memory at JS_OFFSET.
  6. Reads JS_LEN bytes from linear memory at JS_OFFSET + 4.
  7. Returns the JS as a UTF-8 String.

Volatile reads prevent the compiler from constant-folding the marker contents at compile time, which is essential because the marker is patched post-compilation.

CLI Usage

# Single module (backward compatible)
wasm-rquickjs inject-js \
  --input template.wasm \
  --output output.wasm \
  --js main.js

# Multiple modules (order matches BinarySlot module order from crate generation)
wasm-rquickjs inject-js \
  --input template.wasm \
  --output output.wasm \
  --js main.js \
  --js additional1.js \
  --js additional2.js

The --js flag accepts multiple paths. Order must match the BinarySlot module order used during generate-wrapper-crate (primary module first, then additional modules in order).

@vigoo vigoo merged commit 0c489a5 into main Mar 23, 2026
35 of 38 checks passed
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.

2 participants