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)
Tracking
()
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
- Branch: main
- Revision: bf9757d91e2e53b8ab836f2a08bf4c0275fdcd23
- Timestamp: 2026-10-02T21:18:24+00:00
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
- 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)). - 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).
- Call createShapeSnapshot(proto) to record the shape and current slot values.
- 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.
- 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
- Start from an unmodified tree (
git -C /firefox diffempty). - Build the SpiderMonkey debug shell with /app/mozconfigs/mozconfig.js.linux.debug (build dir /firefox/obj-js-debug).
- 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).
- Observe
Assertion failure: slots_[slot] == later.slots_[slot], at js/src/builtin/TestingFunctions.cpp:6948. - 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
| 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 a bug in the testing function itself. Not security-sensitive.
Description
•