CVE-2021-30858: WebKit use-after-free in IndexedDB
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:
- The callee object is a derived class of
ThreadSafeRefCounted<T>
so the cross-thread task will use aRefPtr
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));
});
}
- 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:
- In
IDBRequest::dispatchEvent
a check is added to only change the state of the request if the event is trusted, i.e. internally generated. - In
CrossThreadTask::createCrossThreadTask
the template is modified to ensure that the callee object, in this case theIDBOpenDBRequest
, is aRefPtr
and therefore protected, rather than being a raw reference. This is done by changing the templates to useThreadSafeRefCountedBase
instead ofThreadSafeRefCounted<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:
- In December 2019, what seems to be a similar bug was found in Chrome by ClusterFuzz: https://bugs.chromium.org/p/chromium/issues/detail?id=1032890
- In March 2019, what seems to be a similar bug was found in WebKit. In this case, the use-after-free was on the
IDBDatabase
object. The patch included adding the template toCrossThreadTask::createCrossThreadTask
for callees that are a derived class ofThreadSafeRefCounted<T>
.
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):
- Check whether other
dispatchEvent
functions change state based on non-trusted (custom/script-generated) events. - Check whether tasks that take place in threads other than the origin thread use raw references or pointers.
- 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:
- Monitoring fixes in other platforms who have similar implementations of code. For example, in January 2020 Chrome fixed that
IDBRequest::DispatchEventInternal
changed state in IndexedDB objects based on untrusted events. Making that change in WebKit would have fixed this vulnerability.
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.