Repository navigation
Close the TIFF on writeTIFF() error paths to prevent a later crash - #15
Open
billdenney wants to merge 1 commit into
Open
billdenney wants to merge 1 commit into
billdenney wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13
A failed
writeTIFF()leaves the session corrupted, so the nextreadTIFF()that itself has to take an error path crashes R instead of raising its normal
condition.
Reproducible example
Expected
Error: pkg:tiff: Cannot read TIFF header. Observed, varying run torun:
caught segfault,free(): invalid size, orFatal error: glibc detected an invalid stdio handle— the signature of ause-after-free rather than a null dereference.
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.
Cause
write_tiff()opens the TIFF atsrc/write.c:66with a pointer to thestack-allocated
tiff_job_tdeclared atsrc/write.c:31:TIFFClose()is reached only on the success path (lines 235 and 238). EveryRf_error()sited afterTIFF_Open()longjmps back to R leaving theTIFF*open, the
FILE*unclosed, and — the part that actually crashes —common.c's staticlast_tiffpointing at aTIFFwhose client data is astack frame that no longer exists.
The stale pointer is dereferenced because
TIFF_Open()assignslast_tiffonly after
TIFFClientOpen()returns:libtiff diagnoses an unreadable header inside
TIFFClientOpen(), soTIFFErrorHandler_()runs whilelast_tiffstill refers to the dead writejob, and its first action is
TIFFClose(last_tiff)(common.c:30).This also explains why only a failing read crashes: a successful read never
invokes the error handler, so it merely overwrites
last_tiff.Fix
Route the five
Rf_error()calls sited afterTIFF_Open()through a helperthat closes the TIFF first.
TIFFClose()runsTIFFCloseProc_(), whichfcloses the output file or frees the in-memory buffer and resetslast_tiff— so one call is the complete cleanup for both output targets.read.calready uses this sameTIFFClose()-then-Rf_error()pattern atlines 380 and 385; this applies it to
write.c. Error messages and allsuccess paths are unchanged. +23/−5 in
src/write.c, no other file touched.Moving the
planescheck above thefopenwould fix only the example above:planesis derived per-iteration fromVECTOR_ELT(img_list, ...)inside thewrite loop, so it cannot be hoisted for the image-list case, and the other
four sites crash identically (measured below).
Verification
No
tests/directory exists in this package, so no regression test is addedhere rather than introducing a test framework; the reprex above is the
regression case. Each check below ran in its own
Rscriptprocess with stdinclosed, since a segfault cannot be caught by
tryCatch().Repeating the reprex, since the symptom is nondeterministic:
caught segfault(exit 139), 2 ×invalid stdio handle, 1 ×free(): invalid size(exit 134), all core-dumped.Error: pkg:tiff: Cannot read TIFF header.R CMD build+R CMD check --no-manual: Status: OK. Compiles with nonew warnings.
Not changed here
Deliberately out of scope for a minimal crash fix, but the same class:
read.c:543andread.c:546are post-TIFF_Open()Rf_error()calls withno
TIFFClose(), so a failed tiled-image read should prime the identicalcrash. (
read.c:380/385do close, so these two look like omissions.)read.c:202andwrite.c:67-71leak theFILE*whenTIFF_Open()itselffails. No dangling pointer there —
tiffisNULL— so no crash.last_tiffbefore callingTIFFClientOpen()inTIFF_Open()would make this class unreachable regardless of callerdiscipline, at the cost of leaking a genuinely-live previous TIFF instead of
closing it. That seemed like your call rather than something to fold into a
bug fix.