← All reports

[4] WebCore::Color: destructor leaves a usable out-of-line pointer in freed storage

LowWebCore graphicsUAF

d474526

Low, and only because nothing here is reachable on its own — the commit closes no trigger. What it removes is amplification: without it, any independent UAF on a Color finds a recognisable heap pointer sitting in the freed slot; with it, the same UAF finds zero.

Hardware memory tagging is supposed to make dangling pointers fault rather than resolve, but that guarantee is a property of how the allocator tags memory, and pointers stored in non-canonical shapes fall outside it. WebCore::Color is a value type used across CSS parsing, painting, canvas, and SVG, laid out as a single word m_colorAndFlags that either packs an inline sRGBA color plus flags, or holds a pointer to a ref-counted OutOfLineComponents carrying wide-gamut and float color data. The expectation a hardened allocator sets is that once an object is destroyed, its storage carries no usable capability.

The angle: for defensive value — no trigger is opened or closed here; the hardening matters only when combined with an independent use-after-free on a Color, where it converts a leftover heap pointer into a zero word.

From the commit message:

WebCore::Color is made of a single field that contains flags in the upper bits and can eventually contain a pointer to an OutOfLineComponents as well.

This compact pointer is untagged by libpas, so we should manually clear it to prevent any security issues if the Color object is Use After Free'd.

Annotate secureZeroBytes and secureZeroSpan with NODELETE so the safer-cpp static analyzer can prove that calling them from ~Color() does not run any destructor or free memory.

Source/WebCore/platform/graphics/Color.h

inline Color::~Color()
{
if (isOutOfLine())
asOutOfLine().deref();
+ secureZeroBytes(m_colorAndFlags);
}

Source/WTF/wtf/StdLibExtras.h

template<typename T, std::size_t Extent>
-void secureZeroSpan(std::span<T, Extent> destination)
+void NODELETE secureZeroSpan(std::span<T, Extent> destination)
...
-template<typename T> void secureZeroBytes(T& object)
+template<typename T> void NODELETE secureZeroBytes(T& object)

Two changes work together. Color::~Color() gains a secureZeroBytes(m_colorAndFlags) call after the existing asOutOfLine().deref(), so the compact pointer/flags word is overwritten before the object's storage is released. In StdLibExtras.h, secureZeroSpan and secureZeroBytes are annotated with NODELETE, which lets WebKit's safer-cpp static analyzer prove that invoking them from a destructor does not itself transitively destruct other objects or free memory — without that annotation the analyzer would reject the call as unsafe from a hardened destructor context.

Failure to scrub a pointer-bearing field in a destructor, leaving an exploitable residue if the freed object is reached via use-after-free.

Memory tagging. MTE (Memory Tagging Extension) is an ARMv8.5 hardware feature: each allocation carries a 4-bit tag, pointers carry a matching tag, and a dereference traps when the two disagree. libpas is WebKit's userspace allocator; it integrates with MTE to retag memory on free, which causes most dangling pointers to fault on access rather than resolve to reused memory.

Compact pointers. A "compact pointer" stores a pointer alongside other bits in a single word — here m_colorAndFlags holds a pointer in the upper bits plus flag bits below. Because the value is not stored as a normal tagged pointer, allocator-level retagging logic does not automatically clear or retag it when the containing object is freed. This is the gap that separates a compact pointer from an ordinary member pointer under MTE.

Color's layout. WebCore::Color is a single-word value type. Depending on its flags it either packs an inline sRGBA color or stores a pointer to a ref-counted OutOfLineComponents holding wide-gamut/float color data. Color uses TZone-allocated memory (WTF_MAKE_TZONE_ALLOCATED(Color)), WebKit's type-segregated heap.

secureZeroBytes and NODELETE. secureZeroBytes is a zeroing primitive whose write is guaranteed not to be optimised away by the compiler — the standard problem with memset-to-zero on a dying object is that the compiler can prove the write is unobservable and delete it. NODELETE is a WebKit safer-cpp annotation declaring that a function will not transitively run any destructor or free memory. The annotation exists so the static analyzer can admit calls to such functions from contexts that must not re-enter allocator code — destructors of hardened types being the motivating case, since a destructor that re-enters the allocator during teardown is exactly what hardened destructor contracts are meant to forbid.

Before the fix, ~Color() released the reference to OutOfLineComponents when out-of-line and then left m_colorAndFlags intact in the dead object's storage. For an out-of-line color, that word is the pointer to the object whose reference was just dropped. Because the pointer sits packed alongside flag bits rather than in canonical form, libpas's MTE retagging does not touch it, so the freed slot retains a recognisable, correctly-formed heap pointer even on tagging hardware.

This is not a logic bug producing corruption on its own — no code path in this diff reads the residue. It is the absence of a defensive scrub that would have neutralised the residual capability if some other bug yields a UAF on a Color.

In such a combined scenario the residue is load-bearing in two ways. If an attacker reclaims a freed Color's storage as another type whose first word is read as data, the leftover word discloses the address of an OutOfLineComponents object — heap layout disclosure. If instead the word is dereferenced as a live OutOfLineComponents*, operations against a freed ref-counted object become available, including its deref() path and its reference-count state: type confusion built on top of the upstream UAF rather than on any flaw in Color itself. After the fix, the same reuse reads zero, so the dereference is a deterministic null fault rather than a read/write primitive.

There is no direct exploit path opened or closed by this commit on its own; the hardening only becomes load-bearing in combination with an independent Color UAF. Color is used in the WebContent process (CSS, canvas, painting) and in the GPU process (rendering), so the hardening applies in both renderer-adjacent sandboxes without itself bridging any boundary.

This change is defense-in-depth rather than a fix to a reachable trigger. It strengthens the memory-safety boundary around Color: previously the invariant "a destroyed Color retains no usable pointer to OutOfLineComponents" was not enforced. It does not enlarge attacker capabilities; it shrinks the amplification surface of a hypothetical upstream UAF in any code holding Color values.

Insight: MTE adoption is surfacing a class of hardening gap across WebKit. Compact pointers — pointer bits packed alongside non-pointer flag bits in one word — are invisible to allocator-level retagging, so they survive free in usable form even on tagging hardware. The same shape appears wherever WebKit uses tagged-pointer tricks (PackedPtr, tagged JSValue-like encodings, CompactPointerTuple, CompactRefPtr) inside short-lived value types whose destructors deref but do not scrub, and each is a candidate for the same treatment. The NODELETE plumbing is notable as infrastructure in its own right: it suggests WebKit is formalising a "safe destructor" contract that the static analyzer can check.