Skip to content

fix(script): on_message UB + feat(session): compile_script + create_script_from_bytes#239

Merged
s1341 merged 7 commits into
frida:mainfrom
jhasheng:fridge-fixes
May 20, 2026
Merged

fix(script): on_message UB + feat(session): compile_script + create_script_from_bytes#239
s1341 merged 7 commits into
frida:mainfrom
jhasheng:fridge-fixes

Conversation

@jhasheng

Copy link
Copy Markdown
Contributor

This branch bundles three related script + session improvements that have been in downstream use (the fridge wrapper, plus its Tauri / TUI consumers) for a while.

1. fix(script): on_message handler UB

ScriptHandler::on_message was routed through a callback that did not retain ownership of the user-data pointer correctly, so the handler could be dropped while frida-core still held a raw pointer to it. The dispatch is rerouted through CallbackHandler.script_handler, removing the use-after-free.

Originally filed as #189 but the patch there was never merged; this version is rebased on current main and adopts a regression-safe construction order.

2. feat(session): wrap compile_script

let bytes = session.compile_script(source, &mut ScriptOption::default())?;

Exposes frida_session_compile_script_sync. Returns the bytecode blob so callers can compile once at build or startup, ship the bytes, and skip the JS-runtime parse cost on every attach.

3. feat(session): Session::create_script_from_bytes

let script = session.create_script_from_bytes(&bytes, &mut ScriptOption::default())?;

The companion to (2). Loads precompiled bytecode instead of source — wraps frida_session_create_script_from_bytes_sync.

4. docs(examples): compile_script round-trip

examples/core/compile_script/ walks the matched API pair: compile a source string into bytes, then load those bytes into a fresh session.

Compatibility

  • Existing Session::create_script(source, ...) is unchanged.
  • The new methods are additive; no public signature moves.
  • Patch (1) changes internal callback wiring only — no public API surface change.

Tested

In daily use through fridge + frida-inspector + frida-wechat-tui (Tauri + TUI inspectors built on top of this fork). The UB fix in (1) has been load-tested against high-frequency message floods (Weixin protobuf hooks at thousands of messages per second) without the original double-free.

yw and others added 3 commits May 13, 2026 17:17
…rida#189)

Pre-fix, `handle_message` passed `*mut CallbackHandler` as the
`g_signal_connect_data` user_data, but `call_on_message::<I>` re-cast
that pointer to `*mut I` and dereferenced it:

    let handler: &mut I = &mut *(user_data as *mut I);
    handler.on_message(formatted_msg, data_vec);

This reads I-sized bytes from the start of `CallbackHandler` (the
`channel: (Sender, Receiver)` tuple) and treats them as `I`. It is
undefined behavior, masked only when `I` is a ZST (e.g. the doc
example's `struct Handler;`).

Symptoms reported in frida#189:
- `Arc<Mutex<_>>` handlers crash inside atomic_load.
- Field reads return garbage / refcounts spike (e.g. 1624879470736).
- Poisoned-mutex errors.

For a handler whose fields include a `parking_lot`/`std::sync::Mutex`,
the spurious atomic op on what frida-core treats as `Sender` internals
can also deadlock `script.load_sync` outright if the JS script body
issues a synchronous `send()` — the bogus SRWLOCK address looks held
and `AcquireSRWLockExclusive` never returns.

Fix: `user_data` is already `*mut CallbackHandler`, and
`handle_message` stores the boxed trait object in
`cb.script_handler`. Use that field instead of the type-pun. The `I`
type parameter is kept (and given a `PhantomData::<I>` no-op) so
monomorphization still emits a distinct fn pointer per handler type,
preserving the original ABI for any external code that relied on it.

Regression test (downstream): a `ScriptHandler` whose state includes
a `u64` counter must increment cleanly from 1, 2, 3, ... rather than
starting at a garbage value and corrupting adjacent memory.
frida-core has shipped a bytecode path for years:

  frida_session_compile_script_sync(session, source, opts) -> GBytes
  frida_session_create_script_from_bytes_sync(session, bytes, opts) -> FridaScript

The Python and Node bindings expose both. frida-rust 0.17.2 only
wraps the source-string path through `create_script`, leaving callers
to either drop into frida-sys themselves or recompile on every load.

This adds two thin methods on `Session`:

  fn compile_script(&self, source, &mut opts) -> Result<Vec<u8>>
  fn create_script_from_bytes<'b>(&self, bytes, &mut opts) -> Result<Script<'b>>

`compile_script` copies the returned GBytes into an owned `Vec<u8>`
and `g_bytes_unref`s the original. `create_script_from_bytes` wraps
`g_bytes_new` / `g_bytes_unref` around the C call.

Bytecode is runtime-specific — callers must compile and load with
matching `ScriptOption::set_runtime`. Documented inline.

Use cases this unblocks:
- Startup speedup: skip the JS parse pass at every attach.
- Soft script protection: ship .bin artifacts instead of readable JS.
- Self-attach `compile_script` workflows so build pipelines can emit
  bytecode without spinning up an extra process.
Demonstrates the new Session::compile_script and
Session::create_script_from_bytes pair from 6a92b72:

  source JS -> compile_script -> Vec<u8> bytecode
            -> write to compiled.bin (ship-as-artifact)
            -> read back
            -> create_script_from_bytes -> Script -> load

Attaches to self (pid=0) so it runs out of the box with no
external target, and pins ScriptRuntime::QJS on both ends to
make the runtime-specific-bytecode constraint explicit.
@s1341

s1341 commented May 19, 2026

Copy link
Copy Markdown
Contributor

Please add tests so we can verify that we don't regress.

jhasheng and others added 4 commits May 20, 2026 09:54
…pt_from_bytes

Covers three regression surfaces for 6a92b72:

  1. compile_script returns a non-empty Vec<u8> and the result loads
     back through create_script_from_bytes (full QJS roundtrip via
     attach-to-self / pid=0).
  2. compile_script with an interior-NUL source returns
     Error::CStringFailed without reaching the C side.
  3. Both new methods work with bare ScriptOption::default(), matching
     the contract of create_script.

CI already runs 'cargo test --features=auto-download' on the frida
crate across Linux/macOS/Windows x x86/x86_64, so these run on every
PR without workflow changes.
The merge in 5fe8f72 picked up parts of frida#235 (the `payload.get(N)`
accessors in Exports::call, and the type change from
`SendPayload` to `serde_json::Value`) but dropped two hunks:

  1. The deletion of the now-dead `SendPayload` struct.
  2. The accessor rewrite inside `call_on_message`, which still
     read `msg.payload.r#type`. After the type change this no
     longer compiled (E0609).

Re-apply both hunks so the file matches what frida#235 intended:

  - Remove `SendPayload` (no remaining references in-tree).
  - Match the rpc envelope via `msg.payload.get(0).and_then(...)`,
    consistent with the same pattern at lines 367-368.
cargo test runs tests in parallel by default. Two threads doing
concurrent device.attach(0) / detach() inside the same process
corrupts frida-core's agent state — on Windows the test binary
exits with STATUS_STACK_BUFFER_OVERRUN (0xc0000409); on other
platforms it would be similarly undefined.

Add a static Mutex<()> that every #[test] in this file acquires
for the full attach -> detach span. unwrap_or_else(into_inner)
recovers a poisoned lock so a panic in one test does not cascade
and turn the rest of the suite red.
@s1341

s1341 commented May 20, 2026

Copy link
Copy Markdown
Contributor

Thanks!

@s1341 s1341 merged commit 080e8a9 into frida:main May 20, 2026
16 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