fix: return no preview when a preview file cannot be opened or read - #41855
Merged
Merged
Conversation
SVG::getThumbnail() passed $file->fopen('r') straight into
stream_get_contents() without checking it, and closed the handle on the success
path only. Both produced a 500 where the user should have seen a media-type
icon.
An unopenable file yields false, and stream_get_contents(false) raises a
TypeError. A TypeError is an \Error, not an \Exception, so it went straight past
the handler a few lines below. #41835 fixed this shape in Bitmap; SVG never got
the same treatment.
The guard tests is_resource() rather than comparing against false, because
View::fopen() returns null - not false - for a path isForbiddenFileOrDir()
rejects and for one Filesystem::resolvePath() finds no storage for.
Bitmap::getThumbnail() had that same narrow check, so it is widened here too:
there the null slips past the guard into stream_get_contents() and then into
fclose() in the finally, two uncatchable TypeErrors instead of one.
The read itself can also throw from the wrapper stack - the encryption module
does exactly that on a missing or corrupt key. That is an \Exception, so it was
caught and turned into "no preview" correctly, but fclose() sat after the read
and never ran, holding the descriptor and the shared lock the View wrapper
releases only on close. The read now has its own try/finally, matching Bitmap.
SVG's tests live in their own file rather than in SVGTest, which extends
Provider, needs the database and skips wherever ImageMagick registers no SVG
coder - owncloudci/php:8.3 included. Everything asserted happens before any SVG
is decoded, so the cases run anywhere ext-imagick is present. The leak case
drives a userland wrapper that throws from stream_read(), since a plain fopen()
of an unreadable path fails at open time instead, and asserts release through
the caller's own handle rather than by counting /proc/self/fd, which is
Linux-only. Every case confirmed failing first, including the new null row
against Bitmap's old guard.
A note for the 10.16 backport: on PHP 7.4 stream_get_contents(false) only warns
and returns false, and PHPUnit converts that warning into
PHPUnit\Framework\Error\Warning, which extends \Exception and is therefore
swallowed by the handler in these methods - so the unopenable cases would pass
there without the fix. The same trap applied to #41835's backport.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
|
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a changelog item based on your changes. |
Written for admins rather than reviewers: no PHP type names, no mention of which handler swallowed what. Three things the first draft got wrong about its own PR. The title said "cannot be opened or read", promising a behaviour change on the read path that was never broken - a failing read already produced the icon, it just held the handle. The leak paragraph followed a sentence about the bitmap providers without naming a provider, so it read as covering them too, when #41835 had already put their fclose() in a finally and the leak fixed here is SVG-only. And the entry described only the old behaviour, where changelog/TEMPLATE and the sibling entries pair that with a statement of what now happens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
phil-davis
approved these changes
Sep 24, 2026
phil-davis
left a comment
Contributor
There was a problem hiding this comment.
Looks OK. Some comments are a bit tortuous to read.
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.
SVG::getThumbnail()passed$file->fopen('r')straight intostream_get_contents()without checking it, and closed the handle on the success path only. Both produced a 500 where the user should have seen a media-type icon.This is the same defect #41835 fixed in
Bitmap; SVG never got the same treatment. Split out of #41827 deliberately, so that security fix stays minimal and cherry-pickable.The two failures
Unopenable file.
fopenyieldsfalse, andstream_get_contents(false)raises aTypeError. ATypeErroris an\Error, not an\Exception, so it went straight past the handler a few lines below.The guard tests
is_resource()rather than comparing againstfalse, becauseView::basicOperation()returnsnull— notfalse— for a pathisForbiddenFileOrDir()rejects and for oneFilesystem::resolvePath()finds no storage for.Bitmap::getThumbnail()had the same narrow=== falsecheck, so it is widened here too: there anullslips past the guard intostream_get_contents()and then intofclose()in thefinally, two uncatchableTypeErrors instead of one.Unreadable file. The read itself can throw from the wrapper stack — the encryption module does exactly that on a missing or corrupt key. That is an
\Exception, so it was caught and turned into "no preview" correctly, butfclose()sat after the read and never ran, holding the descriptor and the shared lock theViewwrapper releases only on close. The read now has its owntry/finally, matchingBitmap.Tests
SVG's cases live in their own file rather than in
SVGTest, which extendsProvider, needs the database, and skips wherever ImageMagick registers no SVG coder —owncloudci/php:8.3included. Everything asserted happens before any SVG is decoded, so the cases run anywhereext-imagickis present.The leak case drives a userland stream wrapper that throws from
stream_read(), since a plainfopen()of an unreadable path fails at open time instead. It asserts release through the caller's own handle rather than by counting/proc/self/fd, which is Linux-only, and asserts the wrapper registered and yielded an open handle first so it cannot pass on a false premise.Every case was confirmed failing first: reverting only
SVG.phpgives 2TypeErrors plus theis_resourcefailure, and the newnullrow fails againstBitmap's old guard.Verification
tests/lib/Preview/inowncloudci/php:8.3: 59 tests, 160 assertions, 0 failures, 18 skips (all pre-existing, for the missing SVG coder).php-cs-fixerclean over 2436 files.php -lclean under 7.4.Note for the 10.16 backport
On PHP 7.4
stream_get_contents(false)only warns and returnsfalse, and PHPUnit converts that warning intoPHPUnit\Framework\Error\Warning, which extends\Exceptionand is therefore swallowed by the handler in these methods — so the unopenable cases would pass there without the fix. The same trap applied to #41835's backport.Known gap, not addressed here
Image,TXT,Movie,MP3andOfficehave nofopenguard either, andImage/TXThave notry/catchat all, so the same file still 500s forimage/*andtext/plain— the two most common preview types. Left out to keep this diff reviewable; happy to follow up.🤖 Generated with Claude Code