Skip to content

Retain trace-proxy callback (fix Hermes use-after-free) - #149

Open
beaucollins wants to merge 3 commits into
mainfrom
beaucollins/hermes-trace-proxy-retain
Open

Retain trace-proxy callback (fix Hermes use-after-free)#149
beaucollins wants to merge 3 commits into
mainfrom
beaucollins/hermes-trace-proxy-retain

Conversation

@beaucollins

Copy link
Copy Markdown
Collaborator

Summary

JavaScriptRuntimeTraceProxyCallable stored the wrapped callback as a bare, non-owning JSValue. Under the Hermes engine the callback's pooled ManagedHermesValue could be released to zero and its slot recycled while the proxy still referenced it, so a later invocation hit a dangling handle and threw "Value is not a function".

Fix

Hold the callback as an owning JSValueRef via JSValueRef::makeRetained, retained for the proxy's lifetime and released in the destructor. This matches how the rest of JavaScriptRuntime.cpp retains values kept across native calls (e.g. JSValueRef::makeRetained, JSValueRefHolder::makeRetainedCallback).

Notes

Only reproduces under Hermes (QuickJS's value model doesn't dangle a bare handle) and only when the tracing layer wraps a callback in a trace proxy. Verified with a Hermes build: crash gone across repeated launches.

JavaScriptRuntimeTraceProxyCallable held the wrapped callback as a bare,
non-owning JSValue. Under Hermes the callback's pooled ManagedHermesValue could
be released to zero and its slot recycled while the proxy still referenced it,
so a later invocation hit a dangling handle and threw "Value is not a function".
Hold it as an owning JSValueRef instead, retained for the proxy's lifetime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📊 PR Size: size/XS

Total changes: 6 lines (1 files)

Top files changed:

  • valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.cpp: +3 -3

Size calculated as additions + deletions. Labels: XS (<10), S (<50), M (<250), L (<1000), XL (1000+)

Wraps an inline closure in runtime.makeTraceProxy, forces GC, then invokes the
proxy. Pre-fix under Hermes the unrooted closure was collected and its slot
recycled, so the call threw 'Value is not a function'. Runs across engines via
the parametrized RuntimeFixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added area/runtime Valdi runtime (C++/native) size/S and removed size/XS labels Aug 19, 2026
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

⚠️ Bazel & CI Test Results

Test Suite Result
valdi_web Integration Test ✅ success
Snapshot Tests ✅ success
macOS: C++ & Platform Tests ✅ success
Linux: Registry Validation ❌ cancelled
Valdi Smoke Tests ✅ success
Linux: C++ Tests ❌ cancelled
Linux: Build Compiler ✅ success
Linux: Build & Export ❌ cancelled
API Surface Check ✅ success
Linux: Module Tests ✅ success
Linux: Hotreload Smoke ✅ success

Some tests failed. Please check the workflow logs for details.

🚀 Bazel remote cache is now enabled - future builds will be faster!

Workflow: Valdi CI

runtime.makeTraceProxy is only bound under kTracingEnabled, so the test can only
exercise the bug in a tracing build. Probe for the binding and GTEST_SKIP when
it's absent instead of failing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/runtime Valdi runtime (C++/native) size/S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant