From 77de8a26c434b4fd496e5c46e3a0b2845cf96492 Mon Sep 17 00:00:00 2001 From: ankuper Date: Thu, 2 Jul 2026 17:52:50 -0400 Subject: [PATCH] fix(animation-cache): stop caching a dangling withUnsafeBytes pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DecompressedData.init derives src_ptr/src_size from a Data.withUnsafeBytes closure and stashes it into stream.pointee for later use across many read(bytes:count:) calls. Data's documentation is explicit that the pointer handed to withUnsafeBytes is only valid for the duration of that closure — Data's backing storage can move or be deallocated once it returns, leaving src_ptr dangling for every read() call after init. compression_stream_process then reads through that dangling pointer, corrupting the heap; malloc's own consistency checks catch the corruption later and abort() (confirmed on-device: SIGABRT inside libsystem_malloc.dylib, queue DCTMultiAnimationRenderer-FirstFrame, build 33196, ~40s after launch — loading the first frame of an animated sticker/emoji). Re-derive a live pointer inside a fresh withUnsafeBytes call on every read(), tracking how many source bytes compression_stream_process has already consumed (consumedSrcBytes) so each call resumes from the right offset. compression_stream_process's own src_size/dst_size bookkeeping is unchanged; only the pointer's lifetime handling is fixed. --- .../Sources/DCTAnimationCacheImpl.swift | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift index cd2c8f6e37..397a655730 100644 --- a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift @@ -965,49 +965,62 @@ private final class DecompressedData { private let dataRange: Range private let stream: UnsafeMutablePointer private var isComplete = false - + // Bytes of dataRange already consumed by compression_stream_process. src_ptr + // cannot be cached across calls: it must be re-derived from a live + // withUnsafeBytes pointer every time (see read(bytes:count:) below). + private var consumedSrcBytes = 0 + init?(compressedData: Data, dataRange: Range) { self.compressedData = compressedData self.dataRange = dataRange - + self.stream = UnsafeMutablePointer.allocate(capacity: 1) guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { self.stream.deallocate() return nil } - - self.compressedData.withUnsafeBytes { bytes in - self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) - self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound - } } - + deinit { compression_stream_destroy(self.stream) self.stream.deallocate() } - + func read(bytes: UnsafeMutablePointer, count: Int) throws { if self.isComplete { throw ReadError.didReadToEnd } - - self.stream.pointee.dst_ptr = bytes - self.stream.pointee.dst_size = count - - let status = compression_stream_process(self.stream, 0) - - if status == COMPRESSION_STATUS_ERROR { - self.isComplete = true - throw ReadError.didReadToEnd - } else if status == COMPRESSION_STATUS_END { - if self.stream.pointee.src_size == 0 { + + // The pointer handed to a withUnsafeBytes closure is only valid for the + // duration of that closure (Data's storage can move/deallocate once it + // returns). The previous code stashed it into stream.pointee.src_ptr in + // init and kept using it across later read() calls — a dangling-pointer + // read that corrupted the heap and surfaced later as a malloc abort + // (SIGABRT inside libsystem_malloc, DCTMultiAnimationRenderer-FirstFrame + // queue, build 33196). Re-derive a live pointer here on every call + // instead, resuming from how many source bytes compression_stream_process + // has consumed so far. + try self.compressedData.withUnsafeBytes { (rawBytes: UnsafeRawBufferPointer) -> Void in + self.stream.pointee.src_ptr = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: self.dataRange.lowerBound + self.consumedSrcBytes) + self.stream.pointee.src_size = (self.dataRange.upperBound - self.dataRange.lowerBound) - self.consumedSrcBytes + self.stream.pointee.dst_ptr = bytes + self.stream.pointee.dst_size = count + + let status = compression_stream_process(self.stream, 0) + self.consumedSrcBytes = (self.dataRange.upperBound - self.dataRange.lowerBound) - self.stream.pointee.src_size + + if status == COMPRESSION_STATUS_ERROR { self.isComplete = true + throw ReadError.didReadToEnd + } else if status == COMPRESSION_STATUS_END { + if self.stream.pointee.src_size == 0 { + self.isComplete = true + } + } + + if self.stream.pointee.dst_size != 0 { + throw ReadError.didReadToEnd } - } - - if self.stream.pointee.dst_size != 0 { - throw ReadError.didReadToEnd } }