July 16, 2026
When -1 Becomes a Heap Write: A libtiff TIFFReadAndRealloc() Vulnerability
How an ordinary stream read failure reached pointer arithmetic and caused confirmed heap memory corruption

By Aleens-labs
6 min read
How an ordinary stream read failure reached pointer arithmetic and caused confirmed heap memory corruption
A stream read failed and returned -1. Instead of rejecting that error value, libtiff fed it straight into an internal byte counter. From there, a routine I/O failure quietly turned into a heap out-of-bounds write.
This article documents a vulnerability I reported in libtiff's TIFFReadAndRealloc() function. The issue is tracked as CAN-2026-2032028 while the CVE request remains under review. I am intentionally using the CAN identifier and do not present it as a final CVE assignment.
The confirmed impact is a heap out-of-bounds write and heap memory corruption. I am not claiming remote code execution because instruction-pointer control and a complete exploit chain have not been demonstrated.
Quick Summary
Identifier: CAN-2026-2032028 (CVE request pending)
Project: libtiff
Affected function: TIFFReadAndRealloc()
Source file: libtiff/tif_read.c
Vulnerability type: Heap out-of-bounds write
CWE: CWE-787
Trigger: A negative tmsize_t value returned by an active TIFF read callback
Confirmed impact: Heap memory corruption and process crash
RCE status: Not demonstrated or claimed
Fix status: Fixed upstream in merge request !901
The vulnerable sequence can be summarized as follows:
custom TIFF read callback
-> read fails and returns -1
-> libtiff adds -1 to already_read
-> the error path computes an invalid memset()
-> heap out-of-bounds writecustom TIFF read callback
-> read fails and returns -1
-> libtiff adds -1 to already_read
-> the error path computes an invalid memset()
-> heap out-of-bounds writeWhy This Bug Is Interesting
Many parser vulnerabilities begin with malformed file content. This one begins at the boundary between a parser and its I/O abstraction.
libtiff allows applications to decode data from custom sources through TIFFClientOpen() and TIFFClientOpenExt(). These APIs are useful for memory buffers, virtual filesystems, custom storage backends, network-backed streams, document preview pipelines, and server-side thumbnail services. That flexibility also means callback return values must be validated before they influence internal state.
The security-relevant question was simple: what happens when the active read callback reports an error using a negative return value? In this code path, that value was used before it was validated.
The Vulnerable Code Path
The issue is reachable through public libtiff APIs:
TIFFClientOpen() / TIFFClientOpenExt()
-> TIFFReadEncodedStrip()
-> TIFFFillStrip()
-> TIFFReadRawStripOrTile2()
-> TIFFReadAndRealloc()TIFFClientOpen() / TIFFClientOpenExt()
-> TIFFReadEncodedStrip()
-> TIFFFillStrip()
-> TIFFReadRawStripOrTile2()
-> TIFFReadAndRealloc()Conceptually, the vulnerable logic in TIFFReadAndRealloc() looked like this:
bytes_read = TIFFReadFile(...);
already_read += bytes_read; /* bytes_read can be (tmsize_t)-1 */
if (bytes_read != to_read) {
memset(tif->tif_rawdata + rawdata_offset + already_read,
0,
(size_t)(tif->tif_rawdatasize - rawdata_offset - already_read));
}bytes_read = TIFFReadFile(...);
already_read += bytes_read; /* bytes_read can be (tmsize_t)-1 */
if (bytes_read != to_read) {
memset(tif->tif_rawdata + rawdata_offset + already_read,
0,
(size_t)(tif->tif_rawdatasize - rawdata_offset - already_read));
}When the callback returns -1, already_read can also become -1. In the reproduced path, the subsequent error handling is effectively equivalent to:
memset(tif_rawdata - 1, 0, tif_rawdatasize + 1)memset(tif_rawdata - 1, 0, tif_rawdatasize + 1)The destination starts one byte before the raw-data allocation, while the computed length spans the allocation plus one byte. AddressSanitizer therefore reports a heap-buffer-overflow write.
Why the Ordering Matters
Negative read results commonly represent an I/O failure. A custom TIFF callback may return a negative value after a connection reset, interrupted stream, virtual filesystem error, backend failure, or application-level callback error. The callback's ability to fail is not the vulnerability; failed reads are expected conditions.
The vulnerability is the order of operations:
unsafe:
update internal state
then validate the read result
safe:
validate the read result
then update internal stateunsafe:
update internal state
then validate the read result
safe:
validate the read result
then update internal stateNo negative size should reach offset updates, pointer arithmetic, or memory-length calculations.
Reproducing the Issue
The proof of concept is a self-contained C program named natural_trigger_local_tcp.c. It does not require a separate server, packet-capture tool, or second terminal. A background thread acts as a local TCP peer and naturally produces the failed read.
The test flow is:
- Build the vulnerable libtiff revision with AddressSanitizer and UndefinedBehaviorSanitizer.
- Open a stream-backed TIFF source through
TIFFClientOpen(). - Let the background thread send a minimal TIFF header with strip data expected later in the stream.
- Reset the local TCP connection before the requested strip bytes arrive.
- Allow the callback's real
read()call to return-1withECONNRESET. - Observe the negative result reach
TIFFReadAndRealloc()during encoded strip loading.
The important point is that the callback result comes from an actual failed read operation. The reproducer does not patch libtiff or hardcode -1 inside the vulnerable function.
Build
BUILD=<path-to-libtiff-asan-build>/libtiff
gcc -g -O0 -fsanitize=address,undefined -pthread \
natural_trigger_local_tcp.c \
-o natural_trigger_local \
-I/usr/include/x86_64-linux-gnu \
-L$BUILD \
-ltiff \
-Wl,-rpath,$BUILDBUILD=<path-to-libtiff-asan-build>/libtiff
gcc -g -O0 -fsanitize=address,undefined -pthread \
natural_trigger_local_tcp.c \
-o natural_trigger_local \
-I/usr/include/x86_64-linux-gnu \
-L$BUILD \
-ltiff \
-Wl,-rpath,$BUILDRun
ASAN_OPTIONS=detect_leaks=0 \
LD_LIBRARY_PATH=$BUILD \
./natural_trigger_localASAN_OPTIONS=detect_leaks=0 \
LD_LIBRARY_PATH=$BUILD \
./natural_trigger_localObserved Result
The read callback returned -1 during strip loading:
[read] pos=200 req=2 got=-1 (ECONNRESET -> -1 returned to libtiff)[read] pos=200 req=2 got=-1 (ECONNRESET -> -1 returned to libtiff)AddressSanitizer then reported the out-of-bounds write:
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1025
#0 __asan_memset
#1 TIFFReadAndRealloc libtiff/tif_read.c:133
#2 TIFFReadRawStripOrTile2 libtiff/tif_read.c:743
#3 TIFFFillStrip libtiff/tif_read.c:978
#4 TIFFReadEncodedStrip libtiff/tif_read.c:579
#5 main natural_trigger_local_tcp.c:234ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1025
#0 __asan_memset
#1 TIFFReadAndRealloc libtiff/tif_read.c:133
#2 TIFFReadRawStripOrTile2 libtiff/tif_read.c:743
#3 TIFFFillStrip libtiff/tif_read.c:978
#4 TIFFReadEncodedStrip libtiff/tif_read.c:579
#5 main natural_trigger_local_tcp.c:234ASAN also identified the precise boundary violation:
0x51900000007f is located 1 bytes before 1024-byte region
[0x519000000080, 0x519000000480)0x51900000007f is located 1 bytes before 1024-byte region
[0x519000000080, 0x519000000480)This result confirms heap memory corruption rather than a cleanly handled decode error.
Test Environment
Repository: https://gitlab.com/libtiff/libtiff.git
Tested revision: a94d1ded80b9b46327d3982a3c80043b29f23e2e
Validation date: June 13, 2026
Operating system: WSL Linux on Windows
Architecture: x86_64
Compiler: GCC with -fsanitize=address,undefined
Library build: ASAN and UBSAN enabled
Security Impact
The confirmed primitive is a heap out-of-bounds write. In the reproduced case, the write begins one byte before the raw-data heap allocation and covers the allocated length plus one byte.
The most relevant attack surface is an application that processes attacker-controlled TIFF data through custom or stream-backed libtiff callbacks. Examples include server-side thumbnail generators, document preview systems, media upload pipelines, virtual filesystem integrations, network-backed image ingestion, and geospatial processing services.
In such a deployment, a transport or backend failure may be propagated as a negative tmsize_t return value. If that result reaches the vulnerable function, it can corrupt heap memory in the decoding process. The exact consequence depends on the surrounding application, allocator layout, process isolation, and error handling.
This establishes a potentially high-impact condition for exposed stream-backed services, but it does not by itself prove universal remote exploitability or code execution.
Downstream Consumer Testing
GDAL and ImageMagick were also tested, but the results require careful interpretation.
With a natural syscall-level failure such as EIO or ECONNRESET, the tested GDAL and ImageMagick paths converted the failed read into a zero-byte result. Both produced a clean TIFFFillStrip error, and no ASAN out-of-bounds write was observed.
With callback-level instrumentation that forced the libtiff read callback itself to return -1, both consumers reached TIFFReadAndRealloc() and reproduced the same ASAN heap-buffer-overflow pattern. This confirms that the vulnerable libtiff path is reachable when a downstream callback propagates a negative result. It does not prove that every GDAL or ImageMagick deployment naturally exposes that condition.
The accurate finding is therefore a heap out-of-bounds write in libtiff's custom-I/O callback handling, not a universal GDAL or ImageMagick vulnerability.
Heap Corruption, Not Remote Code Execution
Heap out-of-bounds writes are serious, but a memory-corruption primitive is not automatically remote code execution. The investigation confirmed that a negative callback return reaches TIFFReadAndRealloc(), corrupts its internal state, and causes an out-of-bounds memset() detected by ASAN.
The investigation did not demonstrate instruction-pointer control, a controlled function-pointer overwrite, a reliable ASLR bypass, command execution, or a complete exploit chain. The responsible impact statement is therefore: confirmed heap out-of-bounds write, potentially high impact in exposed stream-backed services, and no confirmed RCE.
Upstream Fix
The fix validates a negative read result before it can influence offsets, pointers, or lengths. Conceptually, a safe pattern is:
bytes_read = TIFFReadFile(
tif,
tif->tif_rawdata + rawdata_offset + already_read,
to_read);
if (bytes_read < 0)
bytes_read = 0; /* Handle the failed read before arithmetic. */
already_read += bytes_read;bytes_read = TIFFReadFile(
tif,
tif->tif_rawdata + rawdata_offset + already_read,
to_read);
if (bytes_read < 0)
bytes_read = 0; /* Handle the failed read before arithmetic. */
already_read += bytes_read;Returning failure immediately on a negative callback result is another valid approach. After the upstream correction, the same condition produced a clean error instead of an out-of-bounds write:
TIFFFillStrip: Read error at scanline 0; got 0 bytes, expected 2.TIFFFillStrip: Read error at scanline 0; got 0 bytes, expected 2.No ASAN heap-buffer-overflow was observed after the fix.
Lessons for Parser Developers
This vulnerability shows that parser security is not limited to file-format bytes. Contracts at API boundaries are equally important. A single negative error value crossed from an external callback into an internal counter, then into pointer arithmetic, and finally into a memory write.
The defensive lessons are straightforward:
- Treat callback return values as untrusted input.
- Reject negative sizes before arithmetic.
- Validate reads before updating offsets or buffer state.
- Test custom-I/O paths with realistic callback failures.
- Include error paths in sanitizer-based testing.
The value that mattered here was not a complex payload. It was simply -1, used in the wrong place.
Disclosure Status
The vulnerability was reported publicly to the libtiff project, and the upstream fix has been merged. A CVE request has been submitted and remains under review, so this article does not present a final CVE assignment.
References
- libtiff vulnerability report: https://gitlab.com/libtiff/libtiff/-/work_items/854
- libtiff upstream fix: https://gitlab.com/libtiff/libtiff/-/merge_requests/901
Closing Thoughts
The parser expected bytes, the callback returned an error, and the internal state accepted that error as a number. That was enough to move a pointer before its allocation and turn routine I/O failure into heap corruption.
Custom I/O APIs deserve the same security scrutiny as the file parser itself. Sometimes the most dangerous input is not hidden inside the file; it is the error code returned while the file is being read.