Assertion failure: idval.isSymbol(), at js/src/vm/Interpreter-inl.h:524 (with Debugger.Frame.eval)
Categories
(Core :: JavaScript: Debugger API, defect, P3)
Tracking
()
People
(Reporter: decoder, Unassigned)
References
(Blocks 2 open bugs)
Details
(Keywords: ai-involved, testcase)
Attachments
(2 files)
Summary
The per-name private-field cache built for Debugger.Frame.prototype.eval does not respect lexical shadowing: when two enclosing classes declare a private name with the same spelling, the outer class's entry overwrites the inner one. This routes private-name accesses through the wrong path (field-access vs. method brand-check), producing a debug-build assertion in js::CheckPrivateFieldOperation and a wrong-brand load in js::LoadAliasedDebugVar. Both defects are reachable from script via Debugger.Frame.eval.
Root cause
js::ScopeContext::cachePrivateFieldsForEval at js/src/frontend/Stencil.cpp:1065 walks scopes inner -> outer and stores each private name in a HashMap using put(name, NameLocation) — last-write-wins. The outermost declaration of any shadowed private name therefore wins:
for (InputScopeIter si(effectiveScope); si; si++) { // inner -> outer
if (si.scope().kind() == ScopeKind::ClassBody) {
for (auto bi = InputBindingIter(scope_ref); bi; bi++) {
if (bi.kind() == BindingKind::PrivateMethod || ...) {
NameLocation loc = NameLocation::DebugEnvironmentCoordinate(bi.kind(), hops, slots);
effectiveScopePrivateFieldCache_->put(parserName, loc); // overwrites inner
}
slots++;
}
}
hops++;
}
The consumer, js::frontend::EmitterScope::lookupPrivate at js/src/frontend/EmitterScope.cpp:1087, trusts that single entry's bindingKind() to decide whether to emit a brand check (GetAliasedDebugVar) or a field access (GetName + CheckPrivateField).
This yields two defects, both confined to the eval-in-frame path (getPrivateFieldLocation asserts enclosingEnvironmentIsDebugProxy_, so GetAliasedDebugVar/js::LoadAliasedDebugVar is reachable exclusively from Debugger.Frame.prototype.eval):
-
Type-invariant violation / assertion (primary). Inner private method shadowed by outer non-method (field, getter/setter, static method, static field). Private methods have their private-name binding optimized away (
BytecodeEmitter::emitNewPrivateNamesatjs/src/frontend/BytecodeEmitter.cpp:12070—isOptimized = true, noemitNewPrivateName); only the synthetic.privateBrandexists. The mis-classified field path emitsGetName "#x"+CheckPrivateField, but the loaded value is not a private-nameSymbol, trippingMOZ_ASSERT(idval.isSymbol())injs::CheckPrivateFieldOperation(js/src/vm/Interpreter-inl.h:524). -
Wrong brand returned by
js::LoadAliasedDebugVar(secondary). When both declarations are private methods, the brand-check path runs butLoadAliasedDebugVar(js/src/vm/Interpreter.cpp:5383) walks the wrong number of hops on the environment chain and returns the outer class's brand. An instance of the outer class then wrongly passes the inner class's brand check, and the inner class's private method is invoked on it — a private-name integrity violation.
A sweep over (outer kind) x (inner kind) x (access form) shows the assertion in (1) fires for every combination where the inner declaration is an instance method and the outer same-named declaration is a non-method, for accesses o.#x, o.#x(), #x in o, o.#x = 5 (20 combinations).
In an optimized build the MOZ_ASSERT in (1) is compiled out, the non-Symbol value flows into HasOwnProperty which converts via ToPropertyKey and reports a benign TypeError. Defect (2) is silent in both builds and is a private-method-dispatch soundness hole.
Reproduction
Debug ASan shell build. Testcases are attached: poc.js (primary, asserts) and poc_wrong_brand.js (secondary, wrong brand).
$ m-c-asan-debug/dist/bin/js --fuzzing-safe poc.js
poc.js (excerpt):
var dbgGlobal = newGlobal({ newCompartment: true });
var dbg = new dbgGlobal.Debugger();
function evalInNewestFrame(code) {
dbg.addDebuggee(this);
var frame = dbg.getNewestFrame().older;
return frame.eval(code);
}
class A {
#x = 99; // outer: private FIELD
trigger() {
class B {
#x() { return "B-method"; } // inner: private METHOD (shadows)
run() { return evalInNewestFrame("this.#x"); }
}
return new B().run();
}
}
new A().trigger();
No special prefs required; default debug ASan build is sufficient. The shell intrinsics used (newGlobal, Debugger) correspond to the chrome-only Debugger API in the browser.
Crash signature (debug ASan)
Assertion failure: idval.isSymbol(), at js/src/vm/Interpreter-inl.h:524
#1 js::CheckPrivateFieldOperation(...) Interpreter-inl.h:524
#2 js::Interpret(...) Interpreter.cpp:2348 (JSOp::CheckPrivateField)
#5 js::ExecuteKernel(...) Interpreter.cpp:858
#6 EvaluateInEnv(...) debugger/Frame.cpp:1158
#7 js::DebuggerGenericEval(...) debugger/Frame.cpp:1249
#8 js::DebuggerFrame::eval(...) debugger/Frame.cpp:1270
AddressSanitizer: SEGV on unknown address 0x000000000000 (WRITE) [from MOZ_CrashSequence]
Full log: crash_debug_asan.txt.
Suggested fix
cachePrivateFieldsForEval should respect lexical shadowing. Since the walk is inner -> outer, the simplest fix is: only insert into the cache when no entry for that name exists yet (i.e. first/innermost declaration wins) — e.g. replace the unconditional put with lookupForAdd+add, or check !cache.has(parserName) before inserting. lookupPrivate then naturally relies on the binding kind of the innermost declaration. Equivalently, key the cache by (name, scope-depth) so shadowed declarations of different kinds are not conflated.
Security impact
- Crash is a debug-build assertion (broken type invariant). Release builds convert the type confusion to a benign
TypeErrorviaToPropertyKeyinHasOwnProperty. No memory corruption observed on this path. - Trigger requires
Debugger.Frame.prototype.eval— chrome-only / shell-only API, not reachable from web content. - Secondary defect (wrong brand from
LoadAliasedDebugVar) is a private-name integrity violation but again only reachable via Debugger API.
Marked sec-audit pending Mozilla's view on Debugger-API-only correctness/integrity bugs.
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
Updated•5 months ago
|
Updated•5 months ago
|
Description
•