Skip to content

fix: release the stream when a bitmap preview cannot be decoded - #41835

Merged
phil-davis merged 2 commits into
masterfrom
fix/oc10-164-bitmap-stream-leak
Sep 16, 2026
Merged

phil-davis merged 2 commits into
masterfrom
fix/oc10-164-bitmap-stream-leak

Conversation

@oc-tmueller

Copy link
Copy Markdown
Contributor

Summary

Bitmap::getThumbnail() opens the file and closes it only on the success path — the catch around getResizedPreview() returns before the fclose(). Every failed decode therefore leaks one file descriptor for the lifetime of the process.

Flagged as out of scope in #41827 ("that's a resource leak, not a security defect, and deserves its own focused PR"), so this is that PR. It targets master and is independent of the #41827/#41834 chain.

Why it matters more than it looks

A failed decode is not an edge case. Any content ImageMagick has no coder for lands in that catch, and #41834 makes throwing a designed outcome — if a build does not register a provider's coder, the pin throws rather than falling back to content sniffing. So occ preview pre-generation, or a cron preview job, over a directory of .heic/.psd files leaks a handle per file until EMFILE.

Second defect, same three lines

$file->fopen('r') was unchecked. A storage that cannot open the file returns false, and stream_get_contents(false) raises a TypeError — an \Error, so it escapes the catch (\Exception) directly underneath and surfaces as a 500 instead of the missing preview every other failure here degrades to. Confirmed, not theorised:

TypeError: stream_get_contents(): Argument #1 ($stream) must be of type resource, false given
  lib/private/Preview/Bitmap.php:89
  lib/private/Preview/Bitmap.php:51

What changed

  • fclose() moves into a finally, so it runs on both paths.
  • A false return from fopen() is handled explicitly and logged.

Tests

New tests/lib/Preview/BitmapStreamTest.php, 3 cases. It asserts the contract directly via is_resource($stream) on the caller's own handle rather than counting descriptors, so it is portable rather than Linux-only.

Confirmed RED before the fix on owncloudci/php:8.3 — 1 failure (the stream must be closed on the failure path) and 1 error (the TypeError above); the success-path case passed unfixed, as the control. GREEN after: 3 tests, 5 assertions.

Verification

  • tests/lib/Preview/ on owncloudci/php:8.3: 55 tests, 152 assertions, 0 failures.
  • make test-php-style: 0 of 2435 files need fixing.
  • php -l clean under PHP 7.4, so this backports to the 10.x line without syntax changes.

Note on sequencing

#41827 modifies these same lines. Whichever lands second needs a trivial rebase — the conflict is confined to the try/catch/fclose block.

🤖 Generated with Claude Code

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) raises a TypeError - an
\Error, so it escapes the \Exception handler directly underneath and surfaces as
a 500 instead of the missing preview every other failure here degrades to.

Closing moves into a finally block, and a false return from fopen() is handled
explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
@oc-tmueller
oc-tmueller requested a review from a team as a code owner September 16, 2026 10:33
@update-docs

This comment was marked as resolved.

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
phil-davis merged commit 0d0a306 into master Sep 16, 2026
31 checks passed
@phil-davis
phil-davis deleted the fix/oc10-164-bitmap-stream-leak branch September 16, 2026 11:41
phil-davis pushed a commit that referenced this pull request Sep 16, 2026
…6] (#41837)

* fix: release the stream when a bitmap preview cannot be decoded [10.16]

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>

* test: make the backported stream tests detect their defects on PHP 7.4

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>

---------

Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.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.

2 participants