Type confusion in [@ js::ProxyGetProperty] due to AliasSet::None on MGuardIsProxy allowing LICM past JSObject::swap
Categories
(Core :: JavaScript Engine: JIT, defect)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
Details
(5 keywords)
Attachments
(2 files)
Summary
A type confusion vulnerability exists in /firefox/js/src/jit/MIROps.yaml where multiple MIR guard instructions (GuardIsProxy, GuardHasProxyHandler, GuardIsNativeObject, GuardIsNotDOMProxy, GuardIsNotProxy) have alias_set: none instead of the upstream alias_set: load: ObjectFields. With AliasSet::None, LICM hoists these guards out of loops past operations that invoke JSObject::swap (e.g., JS_TransplantObject). After a CrossCompartmentWrapper is swapped with a NativeObject mid-loop, Warp-compiled code calls ProxyGetProperty on the NativeObject without re-checking the guard, interpreting the elements_ pointer (pointing to emptyElementsHeader) as a BaseProxyHandler*.
Affected Code
File: /firefox/js/src/jit/MIROps.yaml, lines 2626-2634 (and similar blocks for other guards)
- name: GuardIsProxy
operands:
object: Object
result_type: Object
guard: true
movable: true
congruent_to: if_operands_equal
alias_set: none # BUG: should be load: ObjectFields
generate_lir: true
- name: GuardHasProxyHandler
operands:
object: Object
arguments:
handler: const void*
result_type: Object
guard: true
movable: true
congruent_to: if_operands_and_arguments_equal
alias_set: none # BUG: should be load: ObjectFields
AliasSet::None tells the MIR alias analysis that this instruction reads no mutable state. Since JSObject::swap (invoked by JS_TransplantObject) mutates the object's shape/class in-place, the guards logically do read ObjectFields. With alias_set: none, LICM sees no dependency between the guard and the doTransplant() call inside the loop, and hoists the guard to the pre-header. After the swap changes the CCW into a FakeDOMObject (NativeObject), the loop body's MProxyGetProperty executes against an object that is no longer a proxy.
The crash site is Proxy::getInternal:
const BaseProxyHandler* handler = proxy->as<ProxyObject>().handler();
...
AutoEnterPolicy policy(cx, handler, proxy, id, BaseProxyHandler::GET, true);
ProxyObject::handler() loads from the proxy's reserved slot at the same offset as NativeObject::elements_. On a NativeObject with no elements, this is &emptyElementsHeader (a 24-byte global). AutoEnterPolicy then calls handler->hasSecurityPolicy(), reading mHasSecurityPolicy at offset 0x21 — 9 bytes past the end of emptyElementsHeader.
Correct Pattern
Match upstream MIROps.yaml:
- name: GuardIsProxy
operands:
object: Object
result_type: Object
guard: true
movable: true
congruent_to: if_operands_equal
alias_set:
load: ObjectFields
generate_lir: true
With load: ObjectFields, alias analysis treats any store to ObjectFields (or AliasSet::Any, which effectful VM calls use) as potentially invalidating the guard, preventing hoisting past the transplant call.
Exploit Chain
- Create a
FakeDOMObjectin the main global and add a data property.x. - Call
transplantableObject({object: domObj})to obtain atransplantclosure wrappingJS_TransplantObject. - Create a same-zone, new-compartment global
g1and storedomObjasg1.ccw— this materialises aCrossCompartmentWrappering1's compartment. - Warp-compile a loop in
g1that readsccw.xeach iteration. CacheIR for the CCW property load emitsGuardIsProxy+GuardHasProxyHandler(CrossCompartmentWrapper)followed by aCallProxyGetVM call. - Because both guards have
AliasSet::None, LICM hoists them out of the loop to the pre-header; thedoTransplant()call inside the loop hasAliasSet::Store(Any)but no dependency edge is created. - Set
trigger = trueand re-enterf(). On the first iteration wheredoTransplant()fires,JS_TransplantObjectfinds the existing CCW ing1's wrapper map and performsJSObject::swap, replacing the CCW's guts with aFakeDOMObjectclone (aNativeObject). - On the next iteration, the hoisted guards are not re-executed. Warp calls
ProxyGetProperty(cx, obj, id, vp)withobjnow aNativeObject. proxy->as<ProxyObject>().handler()loadsNativeObject::elements_=&emptyElementsHeaderand treats it asBaseProxyHandler*.AutoEnterPolicyreadshandler->mHasSecurityPolicyat offset 0x21, causing an out-of-bounds read past the 24-byte global. With a NativeObject that has real elements,handlerbecomes a heap pointer and subsequent virtual calls (handler->hasOwn,handler->get) would execute attacker-influenced vtable entries.
Security Impact
Severity: High — exploitable type confusion in the JIT.
Attacker capability: An attacker who can run JavaScript and trigger an object transplant (in the browser, this occurs during document.adoptNode, WindowProxy navigation, or other DOM brain-transplant operations) can cause Warp-compiled code to call proxy handler virtual methods on a controlled pointer. By populating the swapped-in NativeObject's elements_ with heap-sprayed data, the attacker controls the BaseProxyHandler*, leading to arbitrary virtual call hijack and code execution in the content process.
Preconditions: Warp compilation of a property access through a cross-compartment wrapper, followed by a same-zone transplant that reuses the existing wrapper slot. No non-default prefs required; reproduces with --fuzzing-safe.
ASAN Report
==277428==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5ba5a85b4a21 at pc 0x5ba5ab9798e6 bp 0x7ffc649f5360 sp 0x7ffc649f5358
READ of size 1 at 0x5ba5a85b4a21 thread T0
#0 0x5ba5ab9798e5 in js::BaseProxyHandler::hasSecurityPolicy() const /firefox/obj-x86_64-pc-linux-gnu/dist/include/js/Proxy.h:212:43
#1 0x5ba5ab9798e5 in js::AutoEnterPolicy::AutoEnterPolicy(JSContext*, js::BaseProxyHandler const*, JS::Handle<JSObject*>, JS::Handle<JS::PropertyKey>, unsigned int, bool) /firefox/obj-x86_64-pc-linux-gnu/dist/include/js/Proxy.h:633:22
#2 0x5ba5ab9721cb in js::Proxy::getInternal(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::Value>, JS::Handle<JS::PropertyKey>, JS::MutableHandle<JS::Value>) /firefox/js/src/proxy/Proxy.cpp:494:19
#3 0x5ba5ab9721cb in js::ProxyGetProperty(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::PropertyKey>, JS::MutableHandle<JS::Value>) /firefox/js/src/proxy/Proxy.cpp:538:10
#4 0x3aa34ea111ee (<unknown module>)
0x5ba5a85b4a21 is located 31 bytes before global variable 'emptyElementsHeaderShared' defined in '/firefox/js/src/vm/NativeObject.cpp:57' (0x5ba5a85b4a40) of size 24
0x5ba5a85b4a21 is located 9 bytes after global variable 'emptyElementsHeader' defined in '/firefox/js/src/vm/NativeObject.cpp:51' (0x5ba5a85b4a00) of size 24
SUMMARY: AddressSanitizer: global-buffer-overflow /firefox/obj-x86_64-pc-linux-gnu/dist/include/js/Proxy.h:212:43 in js::BaseProxyHandler::hasSecurityPolicy() const
Suggested Fix
Restore alias_set: load: ObjectFields on all affected guard instructions in /firefox/js/src/jit/MIROps.yaml:
- name: GuardIsProxy
...
alias_set:
load: ObjectFields
- name: GuardHasProxyHandler
...
alias_set:
load: ObjectFields
- name: GuardIsNativeObject
...
alias_set:
load: ObjectFields
- name: GuardIsNotDOMProxy
...
alias_set:
load: ObjectFields
- name: GuardIsNotProxy
...
alias_set:
load: ObjectFields
Any MIR guard that reads an object's class/shape must use at minimum AliasSet::Load(ObjectFields) so that stores from JSObject::swap (which writes ObjectFields via its AliasSet::Store(Any) callers) create a dependency edge preventing illegal hoisting.
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
Updated•7 months ago
|
Comment 10•7 months ago
|
||
(In reply to Bugmon [:jkratzer for issues] from comment #0)
Correct Pattern
Match upstream
MIROps.yaml:
| Reporter | ||
Comment 11•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
•