1SEAL-2026-007: heap buffer over-read in Telegram iOS TL deserialization from operator precedence bug
summary
readBytesAsInt32(_:) in Telegram iOS used a mixed
&& / || gate for 1-4 byte reads. because
&& binds tighter than ||, the
count > 0 && count <= 4 side of the condition could
succeed before the remaining-buffer check was enforced.
when TL parsing reached the 3-byte length lane used after a 0xfe bytes
marker, parseBytes could call readBytesAsInt32(3) on
truncated input and still reach memcpy. the result was a bounded read
past the logical end of the tracked buffer.
the report was sent privately to Telegram on 2026-01-30. the public fix landed on
2026-02-04 in commit
8e9cd79855683efb9a3cbf14a1ecd637cfbf7b54
and the corrected implementation is present in tagged state release-12.4.
severity
MEDIUM — CVSS 3.1 Base Score: 5.3
Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CWE: CWE-125 (Out-of-bounds Read)
vendor outcome: accepted by Telegram with a USD 1,500 bounty offer, declined by the researcher.
affected versions
confirmed vulnerable in tagged releases release-11.15, release-12.0,
release-12.3.1, and release-12.3.2.
the vulnerable form was present no later than commit
60a504a9a896cb0828f1d53057705df33d5c0938.
patched versions
fixed by
8e9cd79855683efb9a3cbf14a1ecd637cfbf7b54
and confirmed fixed in release-12.4 and later observed tags such as
release-12.5.
root cause
the vulnerable branch tried to enforce two independent conditions:
- the requested read width is between 1 and 4 bytes
offset + countstays withinbuffer._size
because those predicates were joined with ||, the first condition was enough
to enter the branch for small reads. parseBytes uses the 3-byte length path
after the 0xfe TL marker, so attacker-controlled truncated TL input could
steer execution into memcpy even when fewer than 3 bytes remained.
exploitation chain
- an attacker supplies malformed TL-encoded bytes to a parser path that reaches
parseBytes - the parser observes a
0xfemarker and requestsreadBytesAsInt32(3) - the mixed boolean expression accepts the read before enforcing the remaining-buffer check
memcpyreads past the logical end tracked bybuffer._size- the result is limited adjacent-byte disclosure with crash potential in harsher memory layouts
fix
the public fix rewrote the branch as a fail-closed guard sequence:
- require
count > 0 - require
count <= 4 - require
self.offset + UInt(count) <= self.buffer._size - require
self.buffer.datato be non-nil beforememcpy
timeline
| date | event |
|---|---|
| 2026-01-30 | report emailed privately to Telegram |
| 2026-02-04 | public fix commit 8e9cd79855683efb9a3cbf14a1ecd637cfbf7b54 lands |
| 2026-02-04 | tagged state release-12.4 contains the corrected implementation |
credit
discovered and reported by Oleh Konko (1seal).
user guidance
users should move to release-12.4 or later, or to any build that includes
commit 8e9cd79855683efb9a3cbf14a1ecd637cfbf7b54.
because the bug sits in the shared TL buffer reader, downstream forks or snapshots that
copied the same readBytesAsInt32(_:) implementation should audit for the
same condition and adopt the fixed guard logic.