
Root-cause analysis and patch validation of CVE-2023-52356 in libtiff using AddressSanitizer and GDB.
This lab analyzes CVE-2023-52356 in libtiff, focusing on the behavior of
TIFFReadRGBATileExt when it receives image coordinates outside the valid
image bounds.
The analysis includes reproduction of the original crash, debugging with AddressSanitizer and GDB, root-cause analysis, examination of the upstream patch, analysis of the original reproducer, and verification of the fixed version.
4d0329a451558511triger_input_47The original reproducer was compiled against the vulnerable libtiff build with AddressSanitizer enabled.
The reproducer was executed with the provided trigger file:
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
The execution reproduced a segmentation fault caused by an invalid memory
read. The AddressSanitizer stack trace identified the failure during a
memmove operation called from TIFFReadRGBATileExt.
Using GDB, the crash condition was reproduced with the following runtime values:
row = 34
img.height = 33
tile_ysize = 1
The vulnerable code then executed:
read_ysize = img.height - row;
With the observed values, this calculation is:
33 - 34 = -1
Because read_ysize is unsigned, the result wrapped to:
read_ysize = 4294967295
which is UINT32_MAX.
The value of read_ysize was later used in the source pointer calculation
for memmove.
Immediately before the failing memmove, GDB showed:
read_ysize = 4294967295
read_xsize = 1
tile_ysize = 1
tile_xsize = 1
i_row = 0
raster = 0x7d0ff67e2d40
The destination pointer evaluated to:
0x7d0ff67e2d40
which was the start of the raster buffer.
The source pointer evaluated to:
0x7d13f67e2d38
The calculated source pointer was 17179869176 bytes, approximately
16 GiB, beyond the start of the raster buffer.
Executing the memmove in GDB resulted in:
SIGSEGV, Segmentation fault
The backtrace showed the following crash path:
__sanitizer_internal_memmove
__asan_memmove
TIFFReadRGBATileExt at tif_getimage.c:3345
LLVMFuzzerTestOneInput at poc.cc:61
This confirms that the unsigned underflow in read_ysize produced an
out-of-bounds source offset. The resulting invalid source pointer was then
used by memmove, causing an invalid memory read and a segmentation fault.
The upstream fix added an explicit bounds check before the vulnerable
read_ysize calculation:
if (col >= img.width || row >= img.height)
{
TIFFErrorExtR(tif, TIFFFileName(tif),
"Invalid row/col passed to TIFFReadRGBATile().");
TIFFRGBAImageEnd(&img);
return (0);
}
Using GDB on the fixed version, the following runtime values were observed:
row = 34
img.height = 33
col = 0
img.width = 2047
For these values, the new validation condition evaluates to true because:
row >= img.height
34 >= 33
The function therefore reported:
Invalid row/col passed to TIFFReadRGBATile()
and returned 0.
As a result, execution did not reach the vulnerable calculation:
read_ysize = img.height - row;
This prevents the unsigned underflow observed in the vulnerable version and
stops the invalid value from being used in the later memmove source pointer
calculation.
The original reproducer incorrectly calculates the number of tiles along the Y axis.
The relevant code passes the return value of TIFFGetField() directly as
the Y coordinate to TIFFComputeTile():
TIFFComputeTile(
in_tif,
0,
TIFFGetField(in_tif, TIFFTAG_IMAGELENGTH, &tile_height),
0,
0)
Using GDB, tile_height was observed to contain:
tile_height = 33
However, TIFFGetField() returned:
1
The return value indicates success; it is not the image height. Therefore, the call effectively becomes:
TIFFComputeTile(in_tif, 0, 1, 0, 0)
For this TIFF, GDB showed that this call returns:
2047
This value is a tile index, not the number of tiles along the Y axis.
The original reproducer then used this value in its calculation of
num_tiles_y, resulting in:
num_tiles_y = 63
However, the image dimensions and tile dimensions are:
image_width = 2047
image_height = 33
tile_width = 1
tile_height = 1
Therefore, the correct number of tiles along the Y axis is:
num_tiles_y = 33
The incorrect value of 63 causes the loop to iterate with Y values from
0 through 62, even though the image height is 33 and the valid Y
coordinates are only 0 through 32.
This allows an invalid value such as:
row = 34
to be passed to TIFFReadRGBATileExt.
The vulnerable libtiff version did not reject this out-of-range coordinate
before performing the unsigned img.height - row calculation. This allowed
the invalid API input generated by the reproducer to become a memory-safety
failure.
The maintainer-provided corrected reproducer calculates the number of tiles directly from the image and tile dimensions.
For the same TIFF, the corrected calculation produces:
num_tiles_x = 2047
num_tiles_y = 33
The corrected reproducer was tested against the same vulnerable libtiff build and the same trigger file.
The program exited with status 0, and no AddressSanitizer or SIGSEGV report
was produced.
This supports the maintainer's observation that the original reproducer contains an incorrect tile-count calculation. However, the vulnerable libtiff version still lacked defensive row and column validation, allowing invalid API coordinates to result in a memory-safety failure.
| Reproducer | libtiff Version | Result |
|---|---|---|
| Original PoC | Vulnerable (4d0329a4) | Invalid read and SIGSEGV in memmove |
| Corrected reproducer | Vulnerable (4d0329a4) | Exit status 0, no ASan/SEGV |
| Original PoC | Fixed (51558511) | Invalid row/col rejected, no ASan/SEGV |
The crash in TIFFReadRGBATileExt occurs when an out-of-range image
coordinate reaches the vulnerable libtiff implementation.
In the original reproducer, an incorrect tile-count calculation causes
num_tiles_y to be calculated as 63 instead of the correct value 33.
As a result, the reproducer can pass a row outside the valid image range to
TIFFReadRGBATileExt.
In the vulnerable libtiff version, the row and column arguments were not validated before the calculation:
read_ysize = img.height - row;
For the reproduced case, row was 34 while img.height was 33.
Because read_ysize is unsigned, the subtraction underflowed to
UINT32_MAX. This value was subsequently used in the source pointer
calculation for memmove, producing an invalid memory read and a
segmentation fault.
The upstream fix adds explicit row and column bounds validation before this
calculation. Testing the fixed build confirmed that the same invalid
coordinate is rejected before the underflow and memmove can occur.
The maintainer-provided corrected reproducer was also tested against the vulnerable build using the same TIFF file. It calculated the correct tile count and completed without an AddressSanitizer or SIGSEGV report.
Therefore, this analysis does not establish that the TIFF file alone triggers the memory-safety failure when the API is used correctly. The original reproducer contains an incorrect tile-count calculation, while the historical libtiff implementation lacked defensive bounds validation and allowed the resulting invalid API input to become a memory-safety failure.