Closed Bug 2027984 Opened 7 months ago Closed 7 months ago

Type confusion in [@ js::ProxyGetProperty] due to AliasSet::None on MGuardIsProxy allowing LICM past JSObject::swap

Categories

(Core :: JavaScript Engine: JIT, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2024919

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

  1. Create a FakeDOMObject in the main global and add a data property .x.
  2. Call transplantableObject({object: domObj}) to obtain a transplant closure wrapping JS_TransplantObject.
  3. Create a same-zone, new-compartment global g1 and store domObj as g1.ccw — this materialises a CrossCompartmentWrapper in g1's compartment.
  4. Warp-compile a loop in g1 that reads ccw.x each iteration. CacheIR for the CCW property load emits GuardIsProxy + GuardHasProxyHandler(CrossCompartmentWrapper) followed by a CallProxyGet VM call.
  5. Because both guards have AliasSet::None, LICM hoists them out of the loop to the pre-header; the doTransplant() call inside the loop has AliasSet::Store(Any) but no dependency edge is created.
  6. Set trigger = true and re-enter f(). On the first iteration where doTransplant() fires, JS_TransplantObject finds the existing CCW in g1's wrapper map and performs JSObject::swap, replacing the CCW's guts with a FakeDOMObject clone (a NativeObject).
  7. On the next iteration, the hoisted guards are not re-executed. Warp calls ProxyGetProperty(cx, obj, id, vp) with obj now a NativeObject.
  8. proxy->as<ProxyObject>().handler() loads NativeObject::elements_ = &emptyElementsHeader and treats it as BaseProxyHandler*.
  9. AutoEnterPolicy reads handler->mHasSecurityPolicy at offset 0x21, causing an out-of-bounds read past the 24-byte global. With a NativeObject that has real elements, handler becomes 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.

Attached file poc.js —
Attached file crash_stack.txt —
Group: core-security → javascript-core-security
Duplicate of this bug: 2027985
Duplicate of this bug: 2027988
Duplicate of this bug: 2027989
Duplicate of this bug: 2027990
Duplicate of this bug: 2027991
Duplicate of this bug: 2027992

(In reply to Bugmon [:jkratzer for issues] from comment #0)

Correct Pattern

Match upstream MIROps.yaml:

But Claude, I am the great clown Pagliacci!

Status: UNCONFIRMED → RESOLVED
Closed: 7 months ago
Duplicate of bug: 2024919
Resolution: --- → DUPLICATE

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.

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

Attachment

General

Created:
Updated:
Size: