Skip to content

Fix crash: DecompressedData caches a dangling withUnsafeBytes pointer (SIGABRT, heap corruption)#2220

Open
ankuper wants to merge 1 commit into
TelegramMessenger:masterfrom
ankuper:fix/animation-cache-dangling-pointer
Open

Fix crash: DecompressedData caches a dangling withUnsafeBytes pointer (SIGABRT, heap corruption)#2220
ankuper wants to merge 1 commit into
TelegramMessenger:masterfrom
ankuper:fix/animation-cache-dangling-pointer

Conversation

@ankuper

@ankuper ankuper commented Jul 2, 2026

Copy link
Copy Markdown

Why

DecompressedData.init derives stream.pointee.src_ptr/src_size from a Data.withUnsafeBytes closure and stashes the pointer for later use across many subsequent read(bytes:count:) calls. Data's documentation is explicit that the pointer handed to withUnsafeBytes is only valid for the duration of that closure — the backing storage can move or be deallocated once it returns, leaving src_ptr dangling for every read() call after init returns.

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, on the DCTMultiAnimationRenderer-FirstFrame queue, ~40s after launch while loading the first frame of an animated sticker/emoji.

Fix

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 correct offset. compression_stream_process's own src_size/dst_size bookkeeping is unchanged; only the pointer's lifetime handling is fixed.

Testing

Verified against the crash report that triggered this (SIGABRT via libsystem_malloc.dylib, stack going through DecompressedData.init/deinit and AnimationCacheItemAccessor.loadNextFrame()). Built and ran on-device (iPhone, iOS 26.5) with animated stickers/emoji loading normally post-fix, no regression observed.

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.
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants