Open Bug 2078792 Opened 1 month ago Updated 1 month ago

Assertion failure: slots_[slot] == later.slots_[slot] in [@ ShapeSnapshot::check] after constant-property IC atomizes a non-writable, non-configurable slot

Categories

(Core :: JavaScript Engine: JIT, defect, P3)

defect

Tracking

()

UNCONFIRMED

People

(Reporter: bugmon, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: assertion, testcase)

Attachments

(4 files)

IRGenerator::canOptimizeConstantDataProperty in js/src/jit/CacheIR.cpp atomizes a non-atom string held in a data property slot and writes the atom back with holder->setSlot(prop.slot(), result), without regard to the property's writable and configurable attributes. ShapeSnapshot::check in js/src/builtin/TestingFunctions.cpp requires that the slot of a non-configurable, non-writable data property is bitwise unchanged while the object keeps the same shape, so a snapshot taken before the IC attaches and checked afterwards fails MOZ_RELEASE_ASSERT(slots_[slot] == later.slots_[slot]). The two sides disagree about whether replacing a string with an equal atom counts as mutating the slot.

A script running in the SpiderMonkey shell with testing functions available can abort the process on a release assertion by reading a non-writable, non-configurable string property of a prototype object from IC-enabled code between createShapeSnapshot and checkShapeSnapshot. The demonstrated effect is the assertion only: the replaced and replacing values are equal strings, the write goes through the barriered setSlot, and no memory corruption or script-visible misbehaviour was observed. The assertion site is a testing function, so the testcase depends on createShapeSnapshot and checkShapeSnapshot being exposed.

Build Info

Affected Code

File: js/src/jit/CacheIR.cpp, line 2284-2314

  Value result = holder->getSlot(prop.slot());
  if (result.isGCThing() && !result.toGCThing()->isTenured() &&
      !result.isObject() && !result.isString()) {
    MOZ_ASSERT(result.isBigInt());
    return false;
  }
  ...
  if (result.isString() && !result.toString()->isAtom()) {
    static constexpr size_t MaxLengthForAtomize = 1000;
    if (result.toString()->length() > MaxLengthForAtomize) {
      return false;
    }
    JSAtom* atom = AtomizeString(cx_, result.toString());
    if (!atom) {
      cx_->recoverFromOutOfMemory();
      return false;
    }
    result.setString(atom);
    holder->setSlot(prop.slot(), result);
  }

  return true;

File: js/src/builtin/TestingFunctions.cpp, line 6933-6951

  if (shape_ == later.shape_) {
    MOZ_RELEASE_ASSERT(objectFlags_ == later.objectFlags_);
    MOZ_RELEASE_ASSERT(baseShape_ == later.baseShape_);
    MOZ_RELEASE_ASSERT(slots_.length() == later.slots_.length());
    MOZ_RELEASE_ASSERT(properties_.length() == later.properties_.length());

    for (size_t i = 0; i < properties_.length(); i++) {
      MOZ_RELEASE_ASSERT(properties_[i] == later.properties_[i]);
      // Non-configurable accessor properties and non-configurable, non-writable
      // data properties shouldn't have had their slot mutated.
      PropertyInfo prop = properties_[i].prop;
      if (!prop.configurable()) {
        if (prop.isAccessorProperty() ||
            (prop.isDataProperty() && !prop.writable())) {
          size_t slot = prop.slot();
          MOZ_RELEASE_ASSERT(slots_[slot] == later.slots_[slot]);
        }
      }
    }
  }

File: js/src/vm/Shape.cpp, line 1084-1098

static bool ShouldUseObjectFuseForPrototype(JSObject* obj) {
  if (!obj->is<NativeObject>()) {
    return false;
  }
  return ShouldUseObjectFuses() && JS::Prefs::objectfuse_for_all_protos();
}

// static
bool JSObject::setIsUsedAsPrototype(JSContext* cx, JS::HandleObject obj) {
  js::ObjectFlags flags = {js::ObjectFlag::IsUsedAsPrototype};
  if (ShouldUseObjectFuseForPrototype(obj)) {
    flags.setFlag(js::ObjectFlag::HasObjectFuse);
  }
  return setFlags(cx, obj, flags);
}

The constant-data-property IC path rewrites the holder's slot with an atom. The shape-snapshot checker asserts that such slots never change for non-configurable, non-writable data properties while the shape is unchanged. The holder gets an ObjectFuse because it is used as a prototype.

A separate variant I wrote, which reads the string from a plain (non-prototype) object and uses it as a computed property key in Ion-compiled code, hits the same assertion and does not when run with --no-ion. That is consistent with the Ion slot-atomizing loads (emitMaybeAtomizeSlot in js/src/jit/CodeGenerator.cpp:18249-18320, selected in js/src/jit/Lowering.cpp:4289 and 5521 when usedAsPropertyKey() is set) violating the same checker invariant, though I did not confirm which instruction performed the write. It suggests the mismatch is between in-place atomization in general and the checker, not only the ObjectFuse path.

Exploit Chain

  1. Define a non-writable, non-configurable data property on an object whose value is a non-atom string of at most 1000 characters ("abcdefghijklmnop".repeat(2)).
  2. Pass the object to Object.create so it is marked as a prototype and receives ObjectFlag::HasObjectFuse (js/src/vm/Shape.cpp:1092-1098; javascript.options.objectfuse_for_all_protos defaults to true in StaticPrefList.yaml).
  3. Call createShapeSnapshot(proto) to record the shape and current slot values.
  4. Read the property through the derived object in a function called 100 times. With IC tiers enabled, IRGenerator::canOptimizeConstantDataProperty atomizes the string and stores the atom into the holder's slot (js/src/jit/CacheIR.cpp:2300-2312). With --no-blinterp --no-baseline --no-ion the testcase exits 0.
  5. Call checkShapeSnapshot(snapshot). The shape is unchanged but the slot value differs, so MOZ_RELEASE_ASSERT(slots_[slot] == later.slots_[slot]) at js/src/builtin/TestingFunctions.cpp:6948 fails and the shell aborts.

Steps to Reproduce

  1. Start from an unmodified tree (git -C /firefox diff empty).
  2. Build the SpiderMonkey debug shell with /app/mozconfigs/mozconfig.js.linux.debug (build dir /firefox/obj-js-debug).
  3. Run testcase.js with js_shell_evaluator using js_binary=/firefox/obj-js-debug/dist/bin/js and no extra flags (the evaluator adds --fuzzing-safe).
  4. Observe Assertion failure: slots_[slot] == later.slots_[slot], at js/src/builtin/TestingFunctions.cpp:6948.
  5. Optional: the same testcase aborts at the same assertion on /firefox/obj-firefox-asan/dist/bin/js, since the check is a MOZ_RELEASE_ASSERT.

Security Impact

  • Severity: Low
  • Attacker capability: Abort the JS shell on a release assertion in the shape-snapshot testing function. No memory corruption, information disclosure or incorrect script-visible result was demonstrated; the slot is overwritten with an atom equal to the original string.
  • Preconditions: The script must be able to call the createShapeSnapshot and checkShapeSnapshot testing functions, and IC-enabled execution tiers must be active. No prefs, shell flags or source patches are needed.

ASAN Report

[2795] Assertion failure: slots_[slot] == later.slots_[slot], at js/src/builtin/TestingFunctions.cpp:6948
#01: ???[/firefox/obj-js-debug/dist/bin/js +0x2aa7791]
#02: ???[/firefox/obj-js-debug/dist/bin/js +0x2ae0056]
#03: ???[/firefox/obj-js-debug/dist/bin/js +0x3d2bc95]
#04: ???[/firefox/obj-js-debug/dist/bin/js +0x3d086a8]
#05: ???[/firefox/obj-js-debug/dist/bin/js +0x324c8da]
#06: ??? (???:???)
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==2795==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5633304347a9 bp 0x7ebd47d4b7c0 sp 0x7ebd47d4b7a0 T2795)
==2795==The signal is caused by a READ memory access.
==2795==Hint: address points to the zero page.
    #0 0x5633304347a9 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
    #1 0x5633304347a9 in ShapeSnapshot::check(JSContext*, ShapeSnapshot const&) const /firefox/obj-js-debug/js/src/builtin/./../../../../js/src/builtin/TestingFunctions.cpp:6948:11
    #2 0x56333046d055 in CheckShapeSnapshot(JSContext*, unsigned int, JS::Value*) /firefox/obj-js-debug/js/src/builtin/./../../../../js/src/builtin/TestingFunctions.cpp:7040:13
    #3 0x5633316b8c94 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:528:13
    #4 0x5633316956a7 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:624:12
    #5 0x563330bd98d9 in js::jit::DoCallFallback(JSContext*, js::jit::BaselineFrame*, js::jit::ICFallbackStub*, unsigned int, JS::Value*, JS::MutableHandle<JS::Value>) /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/BaselineIC.cpp:1717:10
    #6 0x08fd5417e00e  ([anon:js-executable-memory]+0xc00e)

UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==2795==ABORTING
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 a bug in the testing function itself. 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: