← All issues

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

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

Commit 작성자가 로컬에서 crash를 재현하지 못했다는 점을 감안해 Medium으로 평가되었습니다. 관찰 가능한 영향은 inflate 초기화된 zlib 상태에서 deflateEnd()를 호출함으로써 발생하는 invalid free이며, heap metadata를 손상시킬 수 있는 undefined behavior를 유발합니다. 다만 실제 exploit 가능성은 zlib 내부 struct layout과 heap 상태에 달려 있으며, diff만으로는 이를 확인할 수 없습니다.

ZStream destructor는 스트림이 압축용으로 초기화되었는지, 압축 해제용으로 초기화되었는지에 관계없이 deflateEnd()를 무조건 호출했습니다. 이번 패치에서는 초기화 모드를 추적하기 위한 m_operation 멤버가 추가되었고, 이를 기반으로 분기 처리가 이루어집니다.

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 };

ZStream 클래스에 Operation 타입의 멤버 변수 m_operation이 추가되었습니다. 이 enum은 CompressionDecompression을 구분하며, initializeIfNecessary() 호출 시점에 값이 저장됩니다. Destructor는 기존의 단순한 if (m_isInitialized) deflateEnd(...) 구조에서 벗어나, early-return guard를 먼저 수행한 뒤 operation에 따라 분기하는 구조로 재작성되었습니다. 압축 모드에서는 deflateEnd, 압축 해제 모드에서는 inflateEnd를 호출합니다. m_operation의 기본값은 Operation::Compression이며, m_isInitialized는 별도로 관리됩니다.

리소스 정리 불일치 — destructor가 실제로 초기화된 리소스 타입과 다른 해제 함수를 호출하는 패턴.

Compression Streams API(CompressionStream / DecompressionStream)는 웹 표준 JavaScript API로, 스트리밍 방식의 압축 및 압축 해제를 제공합니다. 모든 웹 페이지에서 접근 가능합니다. WebKit 구현체는 Source/WebCore/Modules/compression/ZStream 클래스를 통해 zlib을 감싸고 있습니다. zlib은 초기화와 종료를 쌍으로 제공합니다. 압축에는 deflateInit2()/deflateEnd(), 압축 해제에는 inflateInit2()/inflateEnd()를 사용합니다. 두 함수 쌍은 동일한 z_stream 객체 내부에서 서로 완전히 다른 내부 상태를 관리합니다. 할당하는 내부 버퍼도 다르고, 사용하는 함수 포인터도 다르며, bookkeeping 구조도 다릅니다. 초기화에 사용된 함수와 다른 종료 함수를 호출하는 것은 zlib API 계약상 undefined behavior입니다.

근본 원인은 전형적인 리소스 정리 불일치 버그입니다. ZStream 클래스는 zlib을 얇게 감싸는 wrapper로 설계되었지만, 어느 초기화 경로를 거쳤는지를 추적하지 않았습니다. 그 결과 항상 압축 정리 경로가 실행되었습니다. JavaScript에서 DecompressionStream을 생성하면(예: new DecompressionStream('gzip')), 내부 z_streaminflateInit2()를 통해 초기화됩니다. 이후 GC에 의해 수집되거나 scope를 벗어나 소멸될 때 destructor가 deflateEnd(&m_stream)을 호출합니다. 이 시점에서 inflate 내부 상태를 deflate 내부 상태로 오해석하게 됩니다. 결과적으로 zlib은 내부 allocation을 잘못 해석하고, 잘못된 내부 구조에서 파생된 유효하지 않은 포인터에 free()를 호출하게 됩니다.

🔒

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

더 확인하려면 구독해 주세요

🔒

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

더 확인하려면 구독해 주세요