Assertion failure: !id.isPrivateName() in [@ js::Proxy::has] via private-name symbol leaked by getWatchtowerLog()
Categories
(Core :: JavaScript Engine, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: assertion, testcase)
Attachments
(4 files)
Watchtower::watchPropertyAddSlow converts the added property's id to a value with IdToValue(id) and stores it in the Watchtower testing log without excluding private names. When a private field is stamped onto an object registered with addWatchtowerTarget, getWatchtowerLog() returns the raw PrivateName symbol to script as the entry's extra property. Script can then use that symbol as an ordinary property key, so sym in proxy reaches js::Proxy::has and delete proxy[sym] reaches js::Proxy::delete_, both of which assert that the id is not a private name.
A script author with access to the SpiderMonkey testing functions can abort a debug build of the engine with an assertion failure by reading a private-name symbol from the Watchtower testing log and using it as the key of an in or delete operation on a Proxy. The symbol was obtained only through addWatchtowerTarget/getWatchtowerLog, which are JS testing functions and not part of the web-exposed language. On an ASAN non-debug shell the same operations ran without any sanitizer report; the scripted proxy has and deleteProperty traps received the private-name symbol, and the symbol could be used to add a private field to a frozen object and to delete one from an object.
Build Info
- Branch: main
- Revision: 3f73c528a1ae5784ea5e1ee2c5ad3762507395f2
- Timestamp: 2026-10-04T05:17:08+00:00
Affected Code
File: js/src/vm/Watchtower.cpp, line 184-189
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
RootedValue val(cx, IdToValue(id));
if (!AddToWatchtowerLog(cx, "add-prop", obj, val)) {
return false;
}
}
File: js/src/vm/Watchtower.cpp, line 48-50
if (!JS_DefineProperty(cx, logObj, "extra", extra, JSPROP_ENUMERATE)) {
return false;
}
File: js/src/proxy/Proxy.cpp, line 392-393
// Private names shouldn't take this path, but only hasOwn;
MOZ_ASSERT(!id.isPrivateName());
File: js/src/proxy/Proxy.cpp, line 283-285
// Private names shouldn't take this path, as deleting a private name
// should be a syntax error.
MOZ_ASSERT(!id.isPrivateName());
File: js/src/vm/Interpreter.cpp, line 2318-2324
ReservedRooted<JSObject*> obj(&rootObject0, &rref.toObject());
ReservedRooted<jsid> id(&rootId0);
FETCH_ELEMENT_ID(-2, id);
if (!HasProperty(cx, obj, id, &found)) {
goto error;
}
The testing log stores the property id as a value with no private-name filtering, and the log entries are returned to script unchanged. The Proxy entry points for has and delete assume a private name never arrives as a generic property key. The in operator converts any symbol value to an id and calls HasProperty, which for a proxy reaches Proxy::has.
Exploit Chain
- Call addWatchtowerTarget(o) on a plain object so property additions to it are recorded in the Watchtower testing log.
- Stamp a private field onto o by constructing a derived class with
#x = 1whose base constructor returns o; Watchtower::watchPropertyAddSlow logs the PrivateName id as a value. - Call getWatchtowerLog() and read
[0].extra, which is the raw PrivateName symbol (typeof is "symbol", description "#x"). - Evaluate
sym in new Proxy({}, {}); JSOp::In converts the symbol to an id and calls HasProperty, which reaches js::Proxy::has. - Debug build: MOZ_ASSERT(!id.isPrivateName()) at js/src/proxy/Proxy.cpp:393 fails and the shell aborts. The variant
delete proxy[sym]fails the equivalent assertion at Proxy.cpp:285 in js::Proxy::delete_.
Steps to Reproduce
- Build the SpiderMonkey debug shell from the unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug (objdir /firefox/obj-js-debug).
- Run the submitted testcase.js with /firefox/obj-js-debug/dist/bin/js --fuzzing-safe testcase.js (js_shell_evaluator defaults; no extra flags, prefs, or source patches).
- Observe
Assertion failure: !id.isPrivateName(), at js/src/proxy/Proxy.cpp:393with js::Proxy::has called from js::Interpret.
Security Impact
- Severity: Low
- Attacker capability: Abort a debug build of SpiderMonkey with an assertion failure. On an ASAN non-debug shell the same operations produced no sanitizer report; the observed effects were limited to scripted proxy
has/deletePropertytraps being called with a private-name key, a private field being added to a frozen object via Object.defineProperty, and a private field being removed viadelete. No memory corruption was demonstrated. - Preconditions: The script must be able to call the JS testing functions addWatchtowerTarget and getWatchtowerLog (defined in js/src/builtin/TestingFunctions.cpp; available in the JS shell under --fuzzing-safe). No route to the private-name symbol from ordinary web content was demonstrated. The assertion is a MOZ_ASSERT and is compiled only into debug builds.
ASAN Report
[35304] Assertion failure: !id.isPrivateName(), at js/src/proxy/Proxy.cpp:393
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==35304==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55791c00c030 bp 0x7ed762ead1c0 sp 0x7ed762ead110 T35304)
==35304==The signal is caused by a READ memory access.
==35304==Hint: address points to the zero page.
#0 0x55791c00c030 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x55791c00c030 in js::Proxy::has(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::PropertyKey>, bool*) /firefox/obj-js-debug/js/src/proxy/./../../../../js/src/proxy/Proxy.cpp:393:3
#2 0x55791c041a5b in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:2322:14
#3 0x55791c039576 in MaybeEnterInterpreterTrampoline(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:420:10
#4 0x55791c0391b8 in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:498:13
#5 0x55791c03bc50 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:888:10
#6 0x55791c03c0ae in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:918:10
#7 0x55791c2fe0a1 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:544:10
#8 0x55791c2fe288 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:568:10
Variant (`delete p[sym]`), same build:
[35414] Assertion failure: !id.isPrivateName(), at js/src/proxy/Proxy.cpp:285
#1 js::Proxy::delete_(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::PropertyKey>, JS::ObjectOpResult&) js/src/proxy/Proxy.cpp:285:3
#2 proxy_DeleteProperty(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::PropertyKey>, JS::ObjectOpResult&) js/src/proxy/Proxy.cpp:853:8
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
| Reporter | ||
Comment 4•1 month ago
|
||
| Reporter | ||
Comment 5•1 month ago
|
||
This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.
Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.
Updated•1 month ago
|
Updated•1 month ago
|
Comment 6•1 month ago
|
||
This is just a bug in a shell testing function. It's not security-sensitive.
Description
•