Type confusion in [@ js::jit::HasNativeElementPure] due to LICM hoisting MGuardIsNativeObject past object-swapping call
Categories
(Core :: JavaScript Engine: JIT, defect)
Tracking
()
People
(Reporter: bugmon, Unassigned)
Details
(5 keywords)
Attachments
(2 files)
Summary
MGuardIsNativeObject is declared with alias_set: none in js/src/jit/MIROps.yaml, making it appear to LICM as having no memory dependencies. This allows the guard to be hoisted out of a loop past a call that performs JSObject::swap (brain transplant), converting the guarded object into a ProxyObject at the same address. Subsequent in-loop operations that depend on the guard — specifically MCallObjectHasSparseElement → HasNativeElementPure — then operate on a proxy while assuming NativeObject layout, causing type confusion and an out-of-bounds read.
Affected Code
File: js/src/jit/MIROps.yaml, line 2601-2611 (local build)
# Guard the object is a native object.
- name: GuardIsNativeObject
operands:
object: Object
result_type: Object
guard: true
movable: true
congruent_to: if_operands_equal
alias_set: none # <-- BUG: should depend on ObjectFields
generate_lir: true
lir_result_type: none
lir_temps: 1
With alias_set: none and movable: true, LICM treats the guard as loop-invariant even though the object's class (stored in its shape, an object field) can be mutated by JSObject::swap inside the loop. The guard is hoisted to the preheader while its consumer MCallObjectHasSparseElement remains in the loop body, breaking the invariant the consumer relies on.
Crash site: js/src/vm/NativeObject.h:734
uint32_t getDenseInitializedLength() const {
return getElementsHeader()->initializedLength; // elements_ is actually a proxy handler ptr
}
Correct Pattern
- name: GuardIsNativeObject
operands:
object: Object
result_type: Object
guard: true
movable: true
congruent_to: if_operands_equal
alias_set:
load: ObjectFields
generate_lir: true
lir_result_type: none
lir_temps: 1
Declaring a load dependency on ObjectFields prevents LICM from hoisting the guard past any instruction that stores to ObjectFields (including calls, which are treated as storing to all alias sets).
Exploit Chain
- Script obtains a transplantable DOM-like native object and adds a sparse indexed property (
obj[100000] = 2) so thatObject.hasOwn(obj, 100000)takes the sparse-element IC path. - Warp compiles
f().HasPropIRGenerator::tryAttachSparseemitsGuardIsNativeObject(obj)followed byCallObjectHasSparseElementResult(obj, index). - During LICM,
MGuardIsNativeObject(alias_set: none, movable, loop-invariant operand) is hoisted to the loop preheader.MCallObjectHasSparseElementstays in the loop because it is effectful. - On the triggering call,
cb()invokesr.transplant(g), which callsJSObject::swap. The object atdomObj's address becomes aCrossCompartmentWrapperproxy. - On the next loop iteration, JIT code calls
HasNativeElementPurewith the proxy, cast asNativeObject*. obj->containsDenseElement(index)readselements_at theNativeObjectoffset. In aProxyObject, this slot holds the handler pointer&CrossCompartmentWrapper::singleton.getElementsHeader()subtractssizeof(ObjectElements)from that pointer and dereferencesinitializedLength— reading 12 bytes before theCrossCompartmentWrapper::singletonglobal, in the global redzone.
Security Impact
Severity: High (sec-high). This is a JIT type confusion between NativeObject and ProxyObject. The immediate manifestation is an out-of-bounds read of attacker-uncontrolled global data, but the same hoisting pattern applies to any consumer of MGuardIsNativeObject. Other consumers may perform writes or further dependent loads on the confused object, potentially yielding memory corruption. Brain transplants are reachable from web content via document.domain / cross-origin navigation, so this is not shell-only.
Preconditions: Warp compilation of a loop containing a sparse-element hasOwn/in check on a swappable object, with a call inside the loop that can trigger a compartment transplant.
ASAN Report
==124455==ERROR: AddressSanitizer: global-buffer-overflow on address 0x57f186744e14 at pc 0x57f183dde7ca bp 0x7ffcb5586b80 sp 0x7ffcb5586b78
READ of size 4 at 0x57f186744e14 thread T0
#0 0x57f183dde7c9 in js::NativeObject::getDenseInitializedLength() const /firefox/js/src/vm/NativeObject.h:734:33
#1 0x57f183dde7c9 in js::NativeObject::containsDenseElement(unsigned int) const /firefox/js/src/vm/NativeObject.h:730:18
#2 0x57f183dde7c9 in js::jit::HasNativeElementPure(JSContext*, js::NativeObject*, int, JS::Value*) /firefox/js/src/jit/VMFunctions.cpp:2258:12
#3 0x2d1935bb5258 (<unknown module>)
#4 0x2d1935b943dc (<unknown module>)
#5 0x57f1845ee1dd in EnterJit(JSContext*, js::RunState&, unsigned char*) /firefox/js/src/jit/Jit.cpp:107:5
#6 0x57f1845ee1dd in js::jit::MaybeEnterJit(JSContext*, js::RunState&) /firefox/js/src/jit/Jit.cpp:255:10
#7 0x57f18481f7f3 in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:450:32
0x57f186744e14 is located 12 bytes before global variable 'js::CrossCompartmentWrapper::singleton' defined in '/firefox/obj-x86_64-pc-linux-gnu/dist/include/js/Wrapper.h:262' (0x57f186744e20) of size 24
0x57f186744e14 is located 52 bytes after global variable 'vtable for js::CrossCompartmentWrapper' defined in 'Unified_cpp_js_src_proxy0.cpp' (0x57f186744ca0) of size 320
SUMMARY: AddressSanitizer: global-buffer-overflow /firefox/js/src/vm/NativeObject.h:734:33 in js::NativeObject::getDenseInitializedLength() const
Suggested Fix
Change the alias set of GuardIsNativeObject in js/src/jit/MIROps.yaml from none to a load of ObjectFields:
- name: GuardIsNativeObject
operands:
object: Object
result_type: Object
guard: true
movable: true
congruent_to: if_operands_equal
- alias_set: none
+ alias_set:
+ load: ObjectFields
generate_lir: true
This fix is already present on mozilla-central HEAD. Additionally, audit all other Guard* MIR ops that inspect object class/shape for the same alias_set: none mistake.
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
| Reporter | ||
Comment 5•7 months ago
|
||
No valid actions for resolution (DUPLICATE).
Removing bugmon keyword as no further action possible. Please review the bug and re-add the keyword for further analysis.
Updated•2 months ago
|
Description
•