CVE-2021-30858: Use-after-free in WebKit
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=229535
Patch CL: https://github.com/WebKit/WebKit/commit/fbf37d27e313d8d0a150a74cc8fab956eb7f3c59
Bug-Introducing CL: https://github.com/WebKit/WebKit/commit/d5dbfd02054e9f904b27224a598ca1bb8ded5f87
Reporter(s): Anonymous
The Code
Proof-of-concept:
var fontFace1 = new FontFace("font1", "", {});
var fontFaceSet = new FontFaceSet([fontFace1]);
fontFace1.family = "font2";
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:
The vulnerability is a use-after-free due to an unchecked end()
iterator. There was an assert statement: ASSERT(iterator != m_facesLookupTable.end());
, but ASSERT
s don't do anything in release builds. Therefore, even if iterator == m_facesLookupTable.end()
in the release build, nothing would happen and iterator
would still be used.
void CSSFontFaceSet::removeFromFacesLookupTable(const CSSFontFace& face, const CSSValueList& familiesToSearchFor)
{
for (auto& item : familiesToSearchFor) {
String familyName = CSSFontFaceSet::familyNameFromPrimitive(downcast<CSSPrimitiveValue>(item.get()));
if (familyName.isEmpty())
continue;
auto iterator = m_facesLookupTable.find(familyName);
ASSERT(iterator != m_facesLookupTable.end());
bool found = false;
for (size_t i = 0; i < iterator->value.size(); ++i) {
if (iterator->value[i].ptr() == &face) {
found = true;
iterator->value.remove(i);
break;
}
}
ASSERT_UNUSED(found, found);
if (!iterator->value.size())
m_facesLookupTable.remove(iterator);
}
}
In FontFaceSet
a FontFace
is not added to the faces lookup table in addToFacesLookupTable
if the font has already been deemed to be invalid. However, removeFromFacesLookupTable
would still attempt to remove the font, leading to the use-after-free.
Patch analysis:
The patch changes the ASSERT
to an if clause. The function will return if iterator == m_facesLookupTable.end()
, since the item it wishes to remove is not found in the table.
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
It seems equally likely that this vulnerability could have been found via fuzzing or code auditing. The trigger is only 3 lines long so it seems like that a fuzzer could have triggered the vulnerability.
(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):
- Look for other places where there is an
ASSERT
checking that the result offind() != end()
, but the result can still be used even if thatASSERT
fails. - Fuzz the
FontFace
components.
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:
- Change any
ASSERT
statements that verifies that a value doesn't equalend()
toRELEASE_ASSERT
statements. These situations should be caught prior to theRELEASE_ASSERT
, but at least if it happens to not be, theRELEASE_ASSERT
would prevent exploitation. - Memory-safe languages. This is a memory corruption bug so switching to a memory safe language would also prevent this type of vulnerability.
Ideas to mitigate the exploit flow:
N/A
Other potential improvements:
0-day detection methods
There doesn't seem to be any really great options for detection due to the trigger for the vulnerability not doing anything that out of the ordinary other than running the samples in a debug WebKit build.