← All issues

[3] ZStream: deflateEnd() called after inflateInit2() via DecompressionStream

Severity: Medium | Component: WebCore Compression Streams | 43a1b0b

Rated Medium because the observable effect is an invalid free from calling deflateEnd() on inflate-initialized zlib state, and while this produces undefined behavior that could corrupt heap metadata, the commit author was unable to reproduce a crash locally — practical exploitation depends on zlib's internal struct layout and heap state, which are not demonstrable from the diff alone.

The ZStream destructor unconditionally called deflateEnd() regardless of whether the stream was initialized for compression or decompression. The fix adds an m_operation member to track the initialization mode and branches accordingly.

Source/WebCore/Modules/compression/ZStream.cpp

+ m_operation = operation;
m_isInitialized = true;
return true;
}
ZStream::~ZStream()
{
- if (m_isInitialized)
+ if (!m_isInitialized)
+ return;
+
+ if (m_operation == Operation::Compression)
deflateEnd(&m_stream);
+ else
+ inflateEnd(&m_stream);
}

Source/WebCore/Modules/compression/ZStream.h

z_stream m_stream;
+ Operation m_operation { Operation::Compression };
bool m_isInitialized { false };

The patch adds an m_operation member variable of type Operation (an enum distinguishing Compression from Decompression) to the ZStream class, stored during initializeIfNecessary(). The destructor is restructured from a simple if (m_isInitialized) deflateEnd(...) to an early-return guard followed by an operation-dependent branch: deflateEnd for compression, inflateEnd for decompression. The m_operation member defaults to Operation::Compression, and m_isInitialized remains separately tracked.

Mismatched resource cleanup — destructor calls the wrong deallocation function for the resource type that was actually initialized.

The Compression Streams API (CompressionStream and DecompressionStream) is a web-standard JavaScript API for streaming compression/decompression, accessible from any web page. WebKit's implementation wraps zlib via the ZStream class in Source/WebCore/Modules/compression/. zlib uses paired init/end functions: deflateInit2()/deflateEnd() for compression and inflateInit2()/inflateEnd() for decompression. These functions manage completely different internal state structures within the same z_stream object — they allocate different internal buffers, use different function pointers, and maintain different bookkeeping. Calling the wrong end function on an initialized stream is undefined behavior per the zlib API contract.

The root cause is a classic mismatched-cleanup bug. The ZStream class was designed as a thin wrapper around zlib but failed to track which initialization path was taken, defaulting to the compression cleanup path unconditionally. When a DecompressionStream was created from JavaScript (e.g., new DecompressionStream('gzip')), it initialized the internal z_stream via inflateInit2(). On destruction — whether by garbage collection or scope exit — the destructor called deflateEnd(&m_stream), which interprets the inflate internal state as deflate internal state. This causes zlib to misinterpret internal allocations and call free() on invalid pointers derived from the wrong internal structures.

🔒

The heap corruption implications of this mismatched cleanup are explored, including exploitation feasibility from web content

Subscribe to read more

🔒

Multiple audit patterns identified for mismatched resource cleanup across WebKit's native library wrappers

Subscribe to read more