Some Brief Notes on WebKit Heap Hardening

By Sam Brown on 13 April, 2018

Sam Brown

13 April, 2018

Apple recently pushed some substantial heap hardening changes to the allocator used within WebKit and JavaScriptCore (JSC), luckily just after pwn2own, but in order to target Safari again next year these new hardening changes will need to be bypassed or unprotected functionality will need to be targeted.

In the spirit of Chris Rohlf's blog posts, I decided to spend a week taking a time boxed poke at the modified allocator with a few key goals:

  • How does the allocator work with the new changes?
  • What allocations aren't protected by Gigacage? And can these easily be leveraged in exploits?
  • Are any classes of bugs fully mitigated by the changes?

The original commit merging the changes in can be found here: https://github.com/WebKit/webkit/commit/197cd32c3b5527e8c2bbe3fcb7d78cc993dd8904 but there have been some patched since to protect additional items.

The memory allocator code itself can be found within: https://github.com/WebKit/webkit/tree/master/Source/bmalloc/bmalloc

Heaps of Heaps

Historically WebKit had a single heap with no isolation. This is very different to other browsers such as Internet Explorer which began separating DOM objects and other objects in June 2014 (as described on MWR Labs) and even Flash has had some form of separation for exploit prone objects since July 2015 (described by Google Project Zero here).

However with these recent changes WebKit has massively stepped up its game. WebKit now features four separate heaps, along with some further isolation features I'll outline in following sections. These types are outlined in the HeapKind enum found here and shown below.

enum class HeapKind {
Primary,
PrimitiveGigacage,
JSValueGigacage,
StringGigacage
};
static constexpr unsigned numHeaps = 4;

The Primary heap is used for standard allocations made using fastMalloc, whether these allocations are made in WebKit or JSC they'll live in the same heap. 

Meanwhile the the JSValueGigacage is used for objects backed by 'butterflies', these data structures exist within JSC and are explained well in this Phrack article, with the key point shown here: 

Internally, JSC stores both properties and elements in the same memory region and stores a pointer to that region in the object itself.
This pointer points to the middle of the region, properties are stored to the left of it (lower addresses) and elements to the right of it. There
is also a small header located just before the pointed to address that contains the length of the element vector.
This concept is called a "Butterfly" since the values expand to the left and right, similar to the wings of a butterfly. Presumably.
In the following, we will refer to both the pointer and the memory region as "Butterfly".

The PrimitiveGigacage is currently being predominantly used for anything backed by native arrays, for example ArrayBufferViews and Wasm memory allocations.

Finally the fairly self explanatory named StringGigacage is being used for string object allocations, via the StringMalloc function. 

Currently there are only 4 separate heaps available but the current implementation supports up to 21, as can be seen within the ensureGigacage function which is called by WebKit on process creation.

// We just go ahead and assume that 64 bits is enough randomness. That's trivially true right
// now, but would stop being true if we went crazy with gigacages. Based on my math, 21 is the
// largest value of n so that n! <= 2^64.
static_assert(numKinds <= 21, "too many kinds"); 

This clearly shows that there are plans to expand this and create more Giacage based heaps in the future.

Gigacages

A Gigacage is a separate heap used to allocate objects that have been put within a specific class. One the key attack patterns WebKit is attempting to prevent here is for example finding a heap overflow within a JavaScript or DOM object and using this to modify the length field of a string or buffer object. The string or buffer can then be used for arbitrary memory read and/or write to any memory within the new range if the length field has been maxed out.

With these objects contained within a separate heap, it should not be possible to have them placed in memory used by targeted objects (stored in a different Gigacage or heap) in a Use After Free (UAF) exploit or precisely placed near targeted objects that are being used with Out Of Bounds (OOB) read/write bugs.

Memory must be explicitly allocated in a Gigacage using the bmalloc API, by default the HeapKind Primary will be used, leading to memory being allocated on the standard heap, however if a HeapKind enum value for a Gigacage is passed to any of the API functions the appropriate Gigacage will be used. Most uses of Gigacage abstract this out, for example the usage of jsValueMalloc or stringMalloc within the WTF sub project.

Each Gigacage is sized to be big enough that even if you have arbitrary out of bounds access to fields in the object, for example when you've overwritten the length field in an array object, you can't access any memory outside of the given Gigacage. This is explained well in the code here and shown below.

// This is exactly 32GB because inside JSC, indexed accesses for arrays, typed arrays, etc,
// use unsigned 32-bit ints as indices. The items those indices access are 8 bytes or less
// in size. 2^32 * 8 = 32GB. This means if an access on a caged type happens to go out of
// bounds, the access is guaranteed to land somewhere else in the cage or inside the runway.
// If this were less than 32GB, those OOB accesses could reach outside of the cage.
#define GIGACAGE_RUNWAY (32llu * 1024 * 1024 * 1024)

In terms of objects still being allocated on the primary heap, it's quite limited but some interesting types are included.

$ grep -R 'fastMalloc(' * | grep 'cpp'
JavaScriptCore/parser/ParserArena.cpp: char* pool = static_cast(fastMalloc(freeablePoolSize));
JavaScriptCore/runtime/PropertyTable.cpp: , m_index(static_cast(fastMalloc(dataSize())))
JavaScriptCore/b3/testb3.cpp: int32_t* inputPtr = static_cast(fastMalloc(sizeof(int32_t)));
JavaScriptCore/wasm/WasmCodeBlock.cpp: auto* result = new (NotNull, fastMalloc(sizeof(CodeBlock))) CodeBlock(context, mode, moduleInformation, WTFMove(createEmbedderWrapper), throwWasmException);

For example this includes a significant number of the container objects used by DOM objects. 

WebCore/rendering/style/QuotesData.cpp: void* slot = fastMalloc(sizeForQuotesDataWithQuoteCount(quotes.size()));
WebCore/rendering/SimpleLineLayout.cpp: void* slot = WTF::fastMalloc(sizeof(Layout) + sizeof(Run) * runVector.size());
WebCore/contentextensions/NFAToDFA.cpp: m_uniqueNodeIdSetBuffer = static_cast(fastMalloc(byteSize));
WebCore/dom/ElementData.cpp: void* slot = WTF::fastMalloc(sizeForShareableElementDataWithAttributeCount(attributes.size()));
WebCore/dom/ElementData.cpp: void* slot = WTF::fastMalloc(sizeForShareableElementDataWithAttributeCount(m_attributeVector.size()));
WebCore/dom/SpaceSplitString.cpp: SpaceSplitStringData* spaceSplitStringData = static_cast(fastMalloc(sizeToAllocate));
WebCore/css/CSSSelectorList.cpp: m_selectorArray = reinterpret_cast(fastMalloc(sizeof(CSSSelector) * otherComponentCount));
WebCore/css/CSSSelectorList.cpp: m_selectorArray = reinterpret_cast(fastMalloc(sizeof(CSSSelector) * flattenedSize));

This means there's still decent opportunities to exploit UAF's and OOB issues among objects stored on the Primary heap. There's a handy BugZilla issue which is used for tracking improvements to the Gigacage implementation. 

IsoHeap

IsoHeap is another new memory allocation API, every object allocated using IsoHeap will be allocated in dedicated memory pages. This stops objects of another type being allocated in the same memory, helping to prevent to exploitation of UAFs by preventing attackers from turning them into Type Confusion issues. This feature is explained pretty well in this commit

In order to use IsoHeap, the WTF_MAKE_ISO_ALLOCATED_IMPL macro must be added to each protected object, this is defined in IsoMallocInlines.h which just changes the define based on whether IsoHeap is enabled or not. If IsoHeap is enabled the macro expands to the code defined here and is shown below. 

MAKE_BISO_MALLOCED_IMPL(isoType) \
::bmalloc::api::IsoHeap&amp; isoType::bisoHeap() \
{ \
static ::bmalloc::api::IsoHeap heap; \
return heap; \
} \
\
void* isoType::operator new(size_t size) \
{ \
RELEASE_BASSERT(size == sizeof(isoType)); \
return bisoHeap().allocate(); \
} \
\
void isoType::operator delete(void* p) \
{ \
bisoHeap().deallocate(p); \
} \
\
struct MakeBisoMallocedImplMacroSemicolonifier##isoType { }

This defines the typed IsoHeap in use for each object - now when an allocation is made it will use memory contained within the 'directory' of pages related to it's type. This is done in static memory, separate from any of the other heaps. Objects with a different IsoHeap type will not be allocated within any of the pages within this directory. An example usage can be seen in the canvas element: https://github.com/WebKit/webkit/blob/master/Source/WebCore/html/HTMLCanvasElement.cpp#L87

This prevents an object from being free'd, a reference to it being kept and an attacker grooming the heap in such a way that an object of a different type is allocated at the same memory address. 

Since IsoHeap is only being used for objects which use the macro, it's easy to grep for where it's being using it.

$ grep -Rh 'WTF_MAKE_ISO_ALLOCATED_IMPL' *
WTF_MAKE_ISO_ALLOCATED_IMPL(InferredStructure);
#define WTF_MAKE_ISO_ALLOCATED_IMPL(name) struct WTFIsoMallocSemicolonifier##name { }
#define WTF_MAKE_ISO_ALLOCATED_IMPL(name) MAKE_BISO_MALLOCED_IMPL(name)
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFontFaceSrcElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFEMorphologyElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFESpecularLightingElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFontFaceNameElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFontFaceFormatElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGGlyphElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGDocument);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGSymbolElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGEllipseElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGFETileElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGCursorElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGGradientElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGUnknownElement);
WTF_MAKE_ISO_ALLOCATED_IMPL(SVGTSpanElement);
.....

We can see that this has been heavily adopted, covering 399 object types in total (when ignoring the two lines output by grep for the macro names usage in defines).

$ grep -Rh 'WTF_MAKE_ISO_ALLOCATED_IMPL' * | wc -l
401

 And surprisingly not a single DOM object is unprotected!

WebKit/Source/WebCore$ grep -Rv 'WTF_MAKE_ISO_ALLOCATED_IMPL' *.cpp | cut -d ':' -f 1 | uniq
WebCoreDerivedSourcesPrefix.cpp
WebCorePrefix.cpp

JSC - Subspaces

Functionality mirroring IsoHeaps has also been added to JavaScriptCore as IsoSubSpaces, these are SubSpaces within the primary heap space which are only used to allocate objects of a certain size. Any object can be defined to use an isolated SubSpace which will only store objects of it's size, the ISO_SUBSPACE_INIT macro can be used for this. This functionality was initially implemented in this commit

Currently only a limited number of objects are allocated in this way, shown below.

directEvalExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), DirectEvalExecutable)
, functionExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), FunctionExecutable)
, indirectEvalExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), IndirectEvalExecutable)
, inferredTypeSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), InferredType)
, inferredValueSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), InferredValue)
, moduleProgramExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), ModuleProgramExecutable)
, nativeExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), NativeExecutable)
, programExecutableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), ProgramExecutable)
, propertyTableSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), PropertyTable)
, structureRareDataSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), StructureRareData)
, structureSpace ISO_SUBSPACE_INIT(heap, destructibleCellHeapCellType.get(), Structure)
, weakSetSpace ISO_SUBSPACE_INIT(heap, destructibleObjectHeapCellType.get(), JSWeakSet)
, weakMapSpace ISO_SUBSPACE_INIT(heap, destructibleObjectHeapCellType.get(), JSWeakMap)

These SubSpaces are within the standard heap and are only segregated based on object size rather than by type as IsoHeap does. This means they give less security guarantees however given the currently very limited set of objects using it, this likely isn't much of an issue.

There's active plans to implement more IsoSubSpaces if the performance hit is found to be reasonable, to quote the initial commit: 'So far, this patch just puts subtypes of ExecutableBase in IsoSubspaces. If it works, we can use it for more things.'.

Additional Type Confusion Protection

After Spectre and Meltdown were published some significant changes were made to how WebKit validates object types. As Spectre relied on branching security checks to be exploited, they've begun implementing security checks which do not require branch instructions. The way they've done this for validating object types is to 'poison' pointers with a set mask value.

For each type of object they separate out the type information and data information, the type information now includes a constant specific to that object class which is used to poison the data pointer. Any accesses to the data have the poison remove from the pointer before the access takes place. By making the poison values large enough they can cause any invalid poison values used to reference unmapped memory. An example of this can be seen in the implementation of JSWebAssemblyTable.

class JSWebAssemblyTable : public JSDestructibleObject {
public:
typedef JSDestructibleObject Base;
*snip*
Wasm::Table* table() { return m_table.ptr(); }
private:
JSWebAssemblyTable(VM&amp;, Structure*, Ref&&);
void finishCreation(VM&amp;);
static void destroy(JSCell*);
static void visitChildren(JSCell*, SlotVisitor&);
PoisonedRef m_table;
template
using PoisonedBarrier = PoisonedWriteBarrier;
MallocPtr<PoisonedBarrier> m_jsFunctions;
};

Any accesses to the 'm_table' field are going to require unmasking the pointer to it. This makes exploitation of type confusion issues harder as if a poisoned pointer is accessed with the poison value from the wrong type, it will resolve to unmapped memory, crashing the process. 

The poison generation code can be found within WTF/wtf/Poisoned.h. The implementation of this is shown below, this shows how the poison values are generated in such a way that they don't generate valid pointers:

uintptr_t key = cryptographicallyRandomNumber();
#if USE(JSVALUE64) &amp;&amp; !OS(WINDOWS)
key = (key &lt;&lt; 32) ^ (static_cast(cryptographicallyRandomNumber()) &lt;&lt; 3);
// Ensure that the poisoned bits (pointer ^ key) do not make a valid pointer and
// cannot be 0. We ensure that it is zero so that the poisoned bits can also be
// used for a notmal zero check without needing to decoded first.
key |= (static_cast(0x1) &lt;&lt; 63);
// Ensure that the bottom alignment bits are still 0 so that the poisoned bits will
// still preserve the properties of a pointer where these bits are expected to be 0.
// This allows the poisoned bits to be used in place of the pointer by clients that
// rely on this property of pointers and sets flags in the low bits.
key &amp;= ~static_cast(0x7);

This is explained well on the WebKit blog. I'm unsure how complete this mitigation is and haven't dug into the practical implementation or status of it in depth.

Impact on Exploitation

In summary these changes severely limit the impact of DOM based UAFs, make the exploitation of OOB read/write issues more challenging and makes exploitation of at least a significant subset of type confusion based issues harder. Representing a substantial ramp up of the difficulty in exploiting WebKit based browsers such as Safari.

Mitigations within JavaScriptCore are somewhat lagging behind but significant improvements have still been made. This is understandable given that DOM based UAFs have historically been much prevalent and more commonly exploited.