Skip to content

Close the TIFF on readTIFF() tiled-image error paths - #16

Open
billdenney wants to merge 2 commits into
s-u:masterfrom
billdenney:fix-readtiff-error-path-leak
Open

billdenney wants to merge 2 commits into
s-u:masterfrom
billdenney:fix-readtiff-error-path-leak

Conversation

@billdenney

Copy link
Copy Markdown

Stacked on the writeTIFF() fix for #13 — please merge #15 first. The
issue reported the crash only through write_tiff(); read_tiff() has two
instances of the same defect.

Cause

The tiled-image branch raises errors at src/read.c:543 and :546 without
closing the TIFF first:

} else { /* tiled image */
    if (indexed || colormap[0] || bps == 12)
        Rf_error("Indexed and 12-bit tiled images are not supported.");

    if (spp > 1 && config != PLANARCONFIG_CONTIG)
        Rf_error("Planar format tiled images are not supported");

The equivalent checks at :380 and :385 already do TIFFClose(tiff);
first. As in write_tiff(), the TIFF's client data is the stack-allocated
tiff_job_t at src/read.c:173, and common.c's static last_tiff keeps
pointing at it after Rf_error() longjmps out. The next libtiff error makes
TIFFErrorHandler_() call TIFFClose(last_tiff) on a frame that no longer
exists.

Reproducible example

Confirmed against a build that already carries the write_tiff() fix, so
this is an independent reproducer rather than a re-run of the original.

The trigger needs a tiled TIFF with PlanarConfiguration = SEPARATE, which
writeTIFF() cannot produce. This builds a minimal one with no extra
dependencies:

mk_tiled_planar() — 16x16 tiled RGB, PLANARCONFIG_SEPARATE
mk_tiled_planar <- function(path) {
  W <- 16L; H <- 16L; SPP <- 3L; TB <- W * H   # one 16x16 tile per plane, 8 bit
  ifd_off <- 40L; ntags <- 11L
  data_off <- ifd_off + 2L + 12L * ntags + 4L
  toff <- data_off + (0:(SPP - 1L)) * TB
  buf <- raw(data_off + SPP * TB)
  p16 <- function(v) writeBin(as.integer(v), raw(), size = 2L, endian = "little")
  p32 <- function(v) writeBin(as.integer(v), raw(), size = 4L, endian = "little")
  put <- function(b, at, bytes) { b[(at + 1L):(at + length(bytes))] <- bytes; b }
  buf <- put(buf,  0L, c(charToRaw("II"), p16(42L), p32(ifd_off)))
  buf <- put(buf,  8L, p16(c(8L, 8L, 8L)))          # BitsPerSample values
  buf <- put(buf, 16L, p32(toff))                   # TileOffsets values
  buf <- put(buf, 28L, p32(rep(TB, SPP)))           # TileByteCounts values
  # tag, type (3 = SHORT, 4 = LONG), count, value-or-offset
  tags <- list(c(256,3,1,W), c(257,3,1,H), c(258,3,3,8), c(259,3,1,1),
               c(262,3,1,2), c(277,3,1,SPP),
               c(284,3,1,2),                        # PlanarConfiguration = SEPARATE
               c(322,3,1,W), c(323,3,1,H),          # TileWidth / TileLength
               c(324,4,3,16), c(325,4,3,28))
  buf <- put(buf, ifd_off, p16(ntags))
  for (i in seq_along(tags)) {
    tg <- tags[[i]]; at <- ifd_off + 2L + 12L * (i - 1L)
    buf <- put(buf, at, c(p16(tg[1]), p16(tg[2]), p32(tg[3])))
    buf <- put(buf, at + 8L,
               if (tg[2] == 3 && tg[3] == 1) c(p16(tg[4]), p16(0L)) else p32(tg[4]))
  }
  buf <- put(buf, ifd_off + 2L + 12L * ntags, p32(0L))   # next IFD = 0
  for (i in seq_len(SPP))
    buf <- put(buf, toff[i], as.raw((((i - 1L) * 80L) + (0:(TB - 1L))) %% 256L))
  writeBin(buf, path); path
}
p <- mk_tiled_planar(tempfile(fileext = ".tif"))
try(tiff::readTIFF(p), silent = TRUE)
#> "Planar format tiled images are not supported"     <- error at read.c:546

f <- tempfile(fileext = ".tif"); file.create(f)
tiff::readTIFF(f)
#> *** caught segfault ***

Expected Error: pkg:tiff: Cannot read TIFF header.

Environment: tiff 0.1-12, R 4.6.1, x86_64-pc-linux-gnu, Ubuntu 24.04.4,
libtiff 4.5.1+git230720-4ubuntu2.5.

Verification

Each check ran in its own Rscript process with stdin closed, since a
segfault cannot be caught by tryCatch(). Baseline is a build that already
has the write_tiff() fix applied.

Check Sequence Before After
R1 tiled-planar read alone clean error clean error
R2 tiled-planar read -> failed read crashed/hung clean error
R3 tiled-planar read -> valid read works works
R4 tiled read + failed write -> failed read crashed/hung clean error

Repeating R2, since the symptom is nondeterministic: 0/15 clean before,
15/15 clean after.

As with the write path, following the failed read with a successful read
(R3) is harmless: a successful open never invokes the error handler, so it
only overwrites last_tiff rather than dereferencing it.

R CMD build + R CMD check --no-manual: Status: OK. The write_tiff()
regression checks from the parent PR all still pass against this build.

Also included

src/read.c:202 now closes the file handle when TIFF_Open() returns NULL.
This one is defensive rather than observed: libtiff reports open failures
through TIFFErrorHandler_(), which calls Rf_error() and longjmps, so
TIFF_Open() generally does not return at all on failure.

Not changed here

A failed read still leaks the FILE* opened at src/read.c:196 whenever
libtiff raises the error from inside TIFFClientOpen() — the handler
longjmps while f is open and before any TIFF* exists to carry it, so no
site-level fclose() can catch it. Fixing that needs an unwind protector
(or for the error handler to know about the pending FILE*), which seemed
out of scope for a targeted crash fix.

Error messages and all success paths are unchanged.

billdenney and others added 2 commits August 24, 2026 14:42
Fixes s-u#13

write_tiff() opens the TIFF at src/write.c:66 with a pointer to the
stack-allocated tiff_job_t declared at src/write.c:31, but TIFFClose() is
only reached on the success path. Every Rf_error() raised after TIFF_Open()
therefore longjmps back to R leaving the TIFF open, the output FILE*
unclosed, and common.c's static last_tiff pointing at a TIFF whose client
data is a stack frame that no longer exists.

That stale pointer is what crashes. TIFF_Open() assigns last_tiff only
after TIFFClientOpen() returns, so when a later readTIFF() hits an
unreadable header, libtiff raises the error from inside TIFFClientOpen()
while last_tiff still refers to the dead write job. TIFFErrorHandler_()
then calls TIFFClose(last_tiff) on it and dereferences the unwound frame.

This also explains why only a failing read crashes: a successful read never
invokes the error handler, so it merely overwrites last_tiff.

Reproducible example (crashed 10/10 runs before this change, with the
symptom alternating between "caught segfault", "free(): invalid size" and
"glibc detected an invalid stdio handle"; clean 20/20 after):

    bad  <- tempfile(fileext = ".tif")
    good <- tempfile(fileext = ".tif")
    file.create(good)
    try(tiff::writeTIFF(array(runif(80), dim = c(4L, 4L, 5L)), bad),
        silent = TRUE)
    tiff::readTIFF(good)

Route the five Rf_error() calls sited after TIFF_Open() through a helper
that closes the TIFF first. TIFFClose() runs TIFFCloseProc_(), which
fcloses the file or frees the in-memory buffer and resets last_tiff, so it
is the complete cleanup for both output targets. read.c already uses this
same TIFFClose()-then-Rf_error() pattern at lines 380 and 385; this applies
it to write.c.

Error messages and all success paths are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-on to the writeTIFF() fix for
s-u#13, which reported the crash only via
write_tiff(). read_tiff() has two instances of the same defect.

The tiled-image branch raises errors at src/read.c:543 and :546 without
closing the TIFF first, unlike the equivalent checks at :380 and :385. As in
write_tiff(), the TIFF's client data is the stack-allocated tiff_job_t at
src/read.c:173, and common.c's static last_tiff keeps pointing at it after
Rf_error() longjmps out. The next libtiff error makes TIFFErrorHandler_()
call TIFFClose(last_tiff) on a frame that no longer exists.

Confirmed against a build that already carries the write_tiff() fix, so
this is an independent reproducer:

    ## a 16x16 tiled RGB TIFF with PlanarConfiguration = SEPARATE
    try(tiff::readTIFF("tiled_planar.tif"), silent = TRUE)
    #> "Planar format tiled images are not supported"
    f <- tempfile(fileext = ".tif"); file.create(f)
    tiff::readTIFF(f)
    #> *** caught segfault ***

Crashed 15/15 runs before this change, clean 15/15 after. As with the write
path, following the failed read with a *successful* read is harmless: a
successful open never invokes the error handler, so it only overwrites
last_tiff rather than dereferencing it.

Also close the file handle when TIFF_Open() itself returns NULL at
src/read.c:202. That path is defensive rather than observed: libtiff
reports open failures through TIFFErrorHandler_(), which calls Rf_error()
and longjmps, so TIFF_Open() generally does not return at all on failure.

Error messages and all success paths are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant