exploit the possibilities

JavaScript V8 Turbofan Out-Of-Bounds Read

JavaScript V8 Turbofan Out-Of-Bounds Read
Posted May 28, 2019
Authored by saelo, Google Security Research

JavaScript V8 Turbofan may read a Map pointer out-of-bounds when optimizing Reflect.construct.

tags | advisory, javascript
MD5 | 36998fe03e21e2360e63455dcd1824ed

JavaScript V8 Turbofan Out-Of-Bounds Read

Change Mirror Download
V8: Turbofan may read a Map pointer out-of-bounds when optimizing Reflect.construct 



The following JavaScript program (found through fuzzing) triggers an assertion failure in debug builds of the latest v8 (and the current release branch, 7.2.502.28):

function f(arg) {
const o = Reflect.construct(Object,arguments,Proxy);
o.foo = arg;
}

for (let i = 0; i < 10000; i++) {
f(i);
}

// Triggers:
// #
// # Fatal error in ../../src/objects/js-objects-inl.h, line 620
// # Debug check failed: has_prototype_slot().
// #

What happens here is roughly the following:

* The function f is executed in the interpreter (ignition) and is eventually marked for optimization by turbofan
* Turbofan initially translates the call to Reflect.construct to a JSCall operation
* In the inlining phase, JSCallReducer concludes that the JSCall will always call Reflect.construct and then JSCallReducer::ReduceReflectConstruct turns it into a JSCreate operation
* When processing the property store, turbofan attempts to infer the type of the receiver, ending up in NodeProperties::InferReceiverMaps
* InferReceiverMaps reaches the allocation site of the object (the JSCreate operation), which it handles with the following code:

case IrOpcode::kJSCreate: {
if (IsSame(receiver, effect)) {
HeapObjectMatcher mtarget(GetValueInput(effect, 0)); // `Object` in the JS code above
HeapObjectMatcher mnewtarget(GetValueInput(effect, 1)); // `Proxy` in the JS code above
if (mtarget.HasValue() && mnewtarget.HasValue() && // basically check if both are constants
mnewtarget.Ref(broker).IsJSFunction()) {
JSFunctionRef original_constructor =
mnewtarget.Ref(broker).AsJSFunction();
if (original_constructor.has_initial_map()) { [[ 1 ]]
original_constructor.Serialize();
MapRef initial_map = original_constructor.initial_map();
if (initial_map.GetConstructor().equals(mtarget.Ref(broker))) {
*maps_return = ZoneHandleSet<Map>(initial_map.object());
return result;
}
}
}
// We reached the allocation of the {receiver}.
return kNoReceiverMaps;
}
break;
}

From [[ 1 ]] the code then reaches:

bool JSFunction::has_initial_map() {
DCHECK(has_prototype_slot());
return prototype_or_initial_map()->IsMap();
}

Where v8 crashes because the Proxy constructor does not have a prototype slot (which would be at offset 56 from the object):

d8> %DebugPrint(Proxy)
DebugPrint: 0x34129420d541: [Function] in OldSpace
- map: 0x341214d01d59 <Map(HOLEY_ELEMENTS)> [FastProperties]
- function prototype: <no-prototype-slot>
- ...

0x341214d01d59: [Map]
- type: JS_FUNCTION_TYPE
- instance size: 56
- constructor

As such, the access to the prototype_or_initial_map field reads 8 byte out-of-bounds after the Proxy constructor. The proxy object seems to be unique in that it is a constructor but does not have the prototype slot.

Now, at least in the d8 shell, the Proxy object is restored from a snapshot during initialization. Right after it in memory comes its DescriptorArray (storing the attributes of the properties of the Proxy object), which has a valid Map pointer at index 0 (like all objects allocated on the GC heap). Then this code runs:

if (initial_map.GetConstructor().equals(mtarget.Ref(broker))) {
*maps_return = ZoneHandleSet<Map>(initial_map.object());
return result;
}

In this case, the constructor for the DescriptorArray Map is null, so this check fails and Turbofan does not perform the optimization, thus calling into the runtime in the generated code. As such, no incorrect behaviour is observable in release builds (because the DCHECKs are not enabled there). After a bit of playing around I did not find an obvious way to exploit this condition in the latest v8, but below are some ideas:

First of, it is possible to free the descriptor array following the Proxy constructor in memory with the following code:

delete Proxy.revocable; // Create a new Map for Proxy, no references to the previous Map remain
%CollectGarbage(0); // Free the previous Map and with it the DescriptorArray

But so far I haven't managed to reclaim that space with something interesting. Also, it might be possible to instantiate the Proxy constructor again (e.g. through iframes) without it being followed by the DescriptorArray, or there might be other objects that are constructors but do not have the prototype slot, which should also be usable to trigger this bug. This way, a different object could be placed after the Proxy object (from which the OOB read happens). In that case, it might be possible to achieve an observable miscompilation and an exploitable condition because the engine incorrectly infers the Map of the created object or because it constructs an object with some internal/unexpected Map, thus leaking internal objects to script. Finally, it might be possible to control mtarget in the code above to be null, which would also cause the following check to pass for the situation above.

There appear to be similar code paths in which Turbofan assumes that newTarget is either a constructor or has the prototype slot. E.g. the following sample triggers an assertion failure in JSCreateLowering::ReduceJSCreateArray where it assumes that newTarget must be a constructor, which the parseInt function is not (but afterwards again nothing observable happens in release builds because this time has_prototype_slot is false).

function f() {
try {
const o = Reflect.construct(Array,arguments,parseInt);
} catch(e) { }
}

for (let i = 0; i < 10000; i++) {
f();
}

// Triggers:
// #
// # Fatal error in ../../src/compiler/js-create-lowering.cc, line 655
// # Debug check failed: original_constructor.map().is_constructor().
// #


This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available (whichever is earlier), the bug
report will become visible to the public.



Found by: saelo@google.com

Comments

RSS Feed Subscribe to this comment feed

No comments yet, be the first!

Login or Register to post a comment

File Archive:

July 2019

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Jul 1st
    34 Files
  • 2
    Jul 2nd
    15 Files
  • 3
    Jul 3rd
    9 Files
  • 4
    Jul 4th
    8 Files
  • 5
    Jul 5th
    2 Files
  • 6
    Jul 6th
    3 Files
  • 7
    Jul 7th
    1 Files
  • 8
    Jul 8th
    15 Files
  • 9
    Jul 9th
    15 Files
  • 10
    Jul 10th
    20 Files
  • 11
    Jul 11th
    17 Files
  • 12
    Jul 12th
    16 Files
  • 13
    Jul 13th
    2 Files
  • 14
    Jul 14th
    1 Files
  • 15
    Jul 15th
    20 Files
  • 16
    Jul 16th
    27 Files
  • 17
    Jul 17th
    7 Files
  • 18
    Jul 18th
    5 Files
  • 19
    Jul 19th
    0 Files
  • 20
    Jul 20th
    0 Files
  • 21
    Jul 21st
    0 Files
  • 22
    Jul 22nd
    0 Files
  • 23
    Jul 23rd
    0 Files
  • 24
    Jul 24th
    0 Files
  • 25
    Jul 25th
    0 Files
  • 26
    Jul 26th
    0 Files
  • 27
    Jul 27th
    0 Files
  • 28
    Jul 28th
    0 Files
  • 29
    Jul 29th
    0 Files
  • 30
    Jul 30th
    0 Files
  • 31
    Jul 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2019 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close