Open Bug 2078823 Opened 1 month ago Updated 1 month ago

Assertion failure: !id.isPrivateName() in [@ js::Proxy::has] via private-name symbol leaked by getWatchtowerLog()

Categories

(Core :: JavaScript Engine, defect, P3)

defect

Tracking

()

UNCONFIRMED

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

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

  1. Call addWatchtowerTarget(o) on a plain object so property additions to it are recorded in the Watchtower testing log.
  2. Stamp a private field onto o by constructing a derived class with #x = 1 whose base constructor returns o; Watchtower::watchPropertyAddSlow logs the PrivateName id as a value.
  3. Call getWatchtowerLog() and read [0].extra, which is the raw PrivateName symbol (typeof is "symbol", description "#x").
  4. Evaluate sym in new Proxy({}, {}); JSOp::In converts the symbol to an id and calls HasProperty, which reaches js::Proxy::has.
  5. 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

  1. Build the SpiderMonkey debug shell from the unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug (objdir /firefox/obj-js-debug).
  2. 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).
  3. Observe Assertion failure: !id.isPrivateName(), at js/src/proxy/Proxy.cpp:393 with 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/deleteProperty traps 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 via delete. 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
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file testcase.js —
Attached file README.md —

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.

Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3

This is just a bug in a shell testing function. It's not security-sensitive.

Group: javascript-core-security
Keywords: sec-other
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: