fix: release the stream when a bitmap preview cannot be decoded [10.16] - #41837
Open
oc-tmueller wants to merge 2 commits into
Open
oc-tmueller wants to merge 2 commits into
oc-tmueller wants to merge 2 commits into
Conversation
Bitmap::getThumbnail() opened the file and closed it only on the success path. The catch around getResizedPreview() returned without closing, so every failed decode leaked one file descriptor for the lifetime of the process. A preview pre-generation run or a cron preview job over a directory of files ImageMagick has no coder for exhausts the descriptors one file at a time. The open was also unchecked. A storage that cannot open the file returns false rather than throwing, and stream_get_contents(false) cannot report that: on the PHP 7.4 this branch runs on it warns and hands on false, so the real cause is only ever logged as "ImageMagick says: Zero size image string passed", behind an unrelated PHP warning. (On PHP 8, which master runs, the same call raises a TypeError - an \Error, so it escapes the \Exception handler directly underneath and surfaces as a 500. That difference is why the wording here and in the changelog deviates from #41835.) Closing moves into a finally block, and a false return from fopen() is handled explicitly. 10.16 backport of #41835. (cherry picked from commit 0d0a306) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
Both cherry-picked test cases were written against PHP 8 and did not hold on the only version this branch supports. testReturnsFalseWhenTheFileCannotBeOpened asserted the return value, which cannot distinguish anything on 7.4: stream_get_contents(false) merely warns and hands on false, the sanitizer coerces it and Imagick rejects the empty string, so the unpatched code already returns false. The case passed with the fix reverted. What the guard actually removes on 7.4 is the noise - a warning from stream_get_contents(), and an "ImageMagick says:" line blaming ImageMagick for a file it never saw - so it now asserts that no warning is emitted, and is renamed accordingly. The handler honours error_reporting(), so diagnostics the code under test silenced with @ (the sanitizer's own loadXML warning, among any future ones) cannot fail the case; the un-suppressed warning alone detects the regression. testClosesTheStreamWhenDecodingThrows fed an XML payload, which ImageMagick sniffs as SVG. It throws here only because neither owncloudci/php:7.4 nor :8.3 registers an SVG delegate; on a build with librsvg or the internal MSVG renderer the lenient parser returns a blank canvas instead, the decode succeeds and the case fails. Replaced with content no coder claims at all, verified to be reported as format '' rather than format 'SVG' on both images. Confirmed both now fail without the fix - 2 failures, the second listing the warning verbatim - with the success-path case still passing as the control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
jvillafanez
approved these changes
Sep 16, 2026
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.
10.16 backport of #41835.
Description
Bitmap::getThumbnail()opens the file and closes it only on the success path — thecatcharoundgetResizedPreview()returns before thefclose(). Every failed decode therefore leaks one file descriptor for the lifetime of the process.A failed decode is not an edge case: any content ImageMagick has no coder for lands in that
catch. Sooccpreview pre-generation, or a cron preview job, over a directory of.heic/.psdfiles leaks a handle per file untilEMFILE.fclose()moves into afinally, so it runs on both paths.Second defect, same three lines — and it behaves differently on 7.4
$file->fopen('r')was unchecked. This is where the backport genuinely diverges from #41835, so the wording of the commit message, the changelog and one test was adapted rather than cherry-picked verbatim.On PHP 8, which
masterruns,stream_get_contents(false)raises aTypeError— an\Error, so it escapes thecatch (\Exception)directly underneath and surfaces as a 500. On PHP 7.4, the only version this branch supports, the same call merely warns and hands onfalse. Probed directly on both images:Tracing the unfixed 10.16 path for a storage that returns
false, thefalseis coerced by the sanitizer and Imagick rejects the empty string:So on 10.16 this is not a 500 — it is a spurious warning plus a log line blaming ImageMagick for a file that was never opened. Handling the
falseexplicitly replaces both with one accurate message, and keeps the line correct if it is ever run on PHP 8. (DOMDocument::loadXML()warns as well, but behind@, andOC\Log\ErrorHandler::onError()drops@-suppressed diagnostics, so only one warning reachesowncloud.logoutside debug mode — which is why the changelog says "an unrelated warning" where #41835's says nothing about warnings at all.)Tests
tests/lib/Preview/BitmapStreamTest.php, 3 cases. It asserts the contract viais_resource($stream)on the caller's own handle rather than counting descriptors, so it is portable rather than Linux-only.The second commit reworks two of the three cases, because as cherry-picked they did not hold on 7.4:
falsethere (per the trace above) — verified by reverting the fix and watching it pass. What the guard actually removes on 7.4 is the warning, so it now asserts that none is emitted, and is renamed to match. The handler honourserror_reporting(), so@-suppressed diagnostics from anywhere in the path cannot fail it; the un-suppressed warning alone detects the regression.<?xml …?><notanimage>is sniffed as SVG and throws only because neitherowncloudci/php:7.4nor:8.3registers an SVG delegate (no decode delegate … 'SVG'). On a build with librsvg or the internal MSVG renderer, that lenient parser returns a blank canvas, the decode succeeds and the case fails. Replaced with content no coder claims at all, confirmed to be reported as format''rather than'SVG'on both images.Confirmed RED before the fix on
owncloudci/php:7.4: 2 failures — the unclosed stream, and the warning recorded verbatim — withtestClosesTheStreamOnSuccesspassing unfixed as the control. GREEN after: 3 tests, 6 assertions.Verification
All on
owncloudci/php:7.4, PHP 7.4.33, imagick 3.8.1 / ImageMagick 6.9.11-60:tests/lib/Preview/BitmapStreamTest.php: 3 tests, 6 assertions, 0 failures.tests/lib/Preview/: 41 tests, 0 failures, 18 skipped — against a pristine-branch baseline of 38 tests / 18 skipped. Exactly the 3 new tests, no new skips. (All 18 skips are the pre-existingNo SVG provider present; this image registers no SVG coder. Assertion totals drift by a couple between runs becausetests/lib/Preview/Provider.phpfeedsrandom_int()dimensions.)make test-php-style: php-cs-fixer 0 of 2410 files, phpcs clean,OCPSinceCheckerOK.make test-php-phan: exit 0 over 1440 files, nothing reported. Worth running separately from master, since this branch pins phan^5.4(5.5.2) where master pins^6.0.7.lib/private/Preview/Bitmap.php. That count is an artefact of my locallib/composerstate; CI reports "No errors" over the same 1497 files.php -lclean on both changed files.Diff vs master
Only the
(string)$bpcast is absent —mastergained it in #41449 (PHP 8.3 support), 10.16 keepsloadFromData($bp). It is context, not touched by this change; the 3-way cherry-pick preserved the 10.16 form.Related
getResizedPreview()and theuseblock only, with no textual overlap withgetThumbnail(), so it needs no rebase after this lands. The two are complementary: the\RuntimeExceptionfix: prevent arbitrary file write via unsanitized SVG/MVG bitmap previews [10.16] (OC10-164) #41828 throws out ofgetResizedPreview()reaches the samecatch, and is now covered by thefinally.The same class of leak still exists next door —
Preview\Image::getThumbnail()returns early on rejected dimensions without reaching itsfclose(), andSVG/TXTalso feed an uncheckedfopen()intostream_get_contents(). #41835 left those alone too; keeping this backport minimal, they deserve their own issue.🤖 Generated with Claude Code