fix: pin the Imagick coder per provider instead of letting it sniff (OC10-164) - #41832
oc-tmueller wants to merge 9 commits into
Conversation
…OC10-164) isDangerousToDecode() (af3c147) is a deny-list over the libmagic-sniffed type, but the decode that follows re-derives the format independently: readImageBlob()/readImage() with no format set consults Imagick's own ~130-entry magic table, so the coder actually invoked can differ from what the mime check reasoned about. application/postscript and application/pdf are deliberately not denied - Postscript and PDF legitimately decode them - which means PostScript-looking bytes still pass the gate through every other Bitmap provider (SGI, Font, Illustrator, Photoshop, TIFF, Heic), and Imagick's own sniffing then hands them to the Ghostscript delegate anyway. Confirmed directly: libmagic classifies "%!PS-Adobe-3.0..." as application/postscript (not denied), and a bare readImageBlob() on that content picks the PS coder regardless of which provider read it. Pin the coder each provider actually expects instead of leaving Imagick to guess: getImagickFormat() maps each provider's own detected mime type(s) to an explicit Imagick format name, and Bitmap::getResizedPreview() reads through a temporary file with a "FORMAT:path" prefix rather than a bare blob read. SVG.php and Office.php - the only other two Imagick read paths in core - get the same treatment. The pin has to be path-based, not setFormat()+readImageBlob(): the latter does reliably make a mismatched format fail, but for every coder pinned here it also silently skips the actual rasterization step, so setImageFormat('png') ends up with no effect and getImageBlob() returns the original, undecoded bytes - confirmed for all eight Bitmap subclasses, not just the lazily-rendered ones (fonts, HEIC) where it was first suspected. Font's failure mode for foreign content is a safe, empty placeholder image rather than a hard rejection: FreeType fails to parse non-font bytes and Imagick returns a blank canvas without ever reaching a script coder or delegate, so it gets its own, separately-worded test rather than sharing the "must return false" assertion used for SGI/Photoshop/TIFF/Heic. PDF/Postscript/Illustrator are intentionally not covered by the foreign-content regression test: they are the Ghostscript-backed providers PostScript-ish content is not foreign to, and Ghostscript does not respect the pin the same way the other coders do - pinning does not close that specific gap, which is a known, accepted residual bounded to those three providers' own domain and mitigated separately by the ImageMagick policy shipped in owncloud-docker/php#309 (denies the MSL/MVG/MSVG coders regardless of entry point). Adds tests/data/testimage.{ai,heic,psd,sgi,tiff,ttf} - genuine samples of each format, since none existed. No genuine OTF ('OTTO'-tagged) fixture: this environment's Imagick/FreeType delegate cannot decode CFF-outline OpenType fonts at all, confirmed against four real system .otf files, independent of pinning; TTF-tagged content is what's actually exercised in practice for the font-sfnt mime type it shares with OTF. Stacked on af3c147..51ca68b (this branch) rather than opened against master directly, since it touches the same files. Per the discussion on OC10-164, this is an independent follow-up, not a blocker for that PR - and the 10.16 backport (#41828) stays unmerged until this approach is agreed here first. 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. |
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
…one (OC10-164) tests/data/testimage.heic was a real HEVC-encoded photo. The GitHub Actions CI runner's stock libheif1 build only ships AV1 decoder plugins (aomdec/aomenc), not HEVC, so CoderPinningTest's Heic (image/heic) and Heic (image/heif) cases fail there even though the pinning mechanism itself is correct - confirmed by reproducing the exact CI steps (ubuntu:24.04 + the same PPA setup-php uses + plain apt-get install imagemagick) locally. Swap in an AVIF-encoded file under the same filename instead: the HEIC coder module decodes both HEVC- and AV1-encoded content, and an AV1-only libheif matches what CI actually has installed. Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
…disk (OC10-164) getResizedPreview()'s coder pin needs a real file path for Imagick to read from, but that file is written, read, and deleted again within the same function call - it never needs to survive on disk at all, so writing it there on every single bitmap/vector preview is unnecessary I/O. Add TempManager::getRamTemporaryFile(), which prefers a tmpfs mount (/dev/shm by default, configurable via the new 'ramtempdirectory' system config key) and falls back transparently to the regular disk-backed temporary directory if none is available or a specific write to it fails. Bitmap::getResizedPreview() now sources its pin file from this method instead of the disk-backed getTemporaryFile(). Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
…4 changelog entry Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
…ce (OC10-164) Bitmap::getResizedPreview() calls it through \OC::$server->getTempManager(), which is typed to the ITempManager interface, not the concrete TempManager class - so Phan correctly flagged it as an undeclared method. Unlike overrideTempBaseDir(), which is only ever called on a concrete-typed test variable, this method is used from real production code and needs to be part of the actual contract. ITempManager has exactly one implementer (\OC\TempManager), so adding a method to it is safe. Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
Code reviewFound 1 issue:
core/lib/private/Preview/SVG.php Lines 65 to 67 in 34769f5 Compare the bitmap path, which does use the tmpfs-backed call: core/lib/private/Preview/Bitmap.php Lines 114 to 116 in 34769f5 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…yFile() (OC10-164) Minimum PHP for this codebase is 8.3, so this newly-added method can state its string $postFix / string|false return contract with real declarations instead of relying purely on the docblock. Also correct the @SInCE tag to 11.0.1 - 11.0.0 has already shipped. Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
Performance: disk-backed vs. RAM-backed coder pinningMeasured
The disk-backed temp file roughly doubled mean/median latency vs. the unpinned baseline. The RAM-backed version brings median and min essentially back to baseline (127ms vs. 126ms, 70.6ms vs. 69.2ms) - the coder pin itself is no longer the dominant cost. Caveats:
|
The SVG provider's coder pin used the disk-backed getTemporaryFile(), while Bitmap.php uses getRamTemporaryFile() for the identical write-read-unlink file. Before the pin was introduced the SVG path was a pure in-memory readImageBlob(), so as it stood this added a real disk write, read and unlink to every SVG preview - contradicting the changelog entry's claim that the pin adds no disk I/O to preview generation. Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
…(OC10-164) getRamTemporaryFile() falls back to the disk-backed directory when no writable tmpfs mount is found or when the temp file cannot be created there. It does not fall back when a later write to an already-created file fails, so drop that claim from the config documentation. Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
|
Closing in favour of #41834, which implements the same coder pin without a temporary file. The reason this PR needed one was the note in its own commit message and code comments: that
That removes the whole reason for
#41834 also fixes something this PR's tests got wrong: they gated every case on Two review points from here are carried over to #41834: No changes are needed to #41827, and #41828 stays held per the agreed sequencing. |
Summary
Follow-up to #41827 (this PR's base branch - stacked, not against master, since it touches the same files). Closes the residual bypass class discussed on OC10-164: mechanism-only writeup at #41827 (comment).
isDangerousToDecode()is a deny-list over the libmagic-sniffed type, but the decode that follows re-derives the format independently:readImageBlob()/readImage()with no format set consults Imagick's own ~130-entry magic table.application/postscript/application/pdfare deliberately not denied (Postscript/PDF need them), so PostScript-looking bytes still pass the gate through every other Bitmap provider (SGI, Font, Illustrator, Photoshop, TIFF, Heic) - and Imagick's own sniffing then hands them to the Ghostscript delegate anyway, regardless of which provider read them.Confirmed directly against the patched code: libmagic classifies
"%!PS-Adobe-3.0..."asapplication/postscript(not denied by the mime gate), and a barereadImageBlob()on that content picks thePScoder - independent of which provider's mime-type check let it through.Changes
getImagickFormat(string $mimeType): stringon everyBitmapsubclass, mapping each provider's own detected mime type(s) to the exact Imagick coder it needs.Bitmap::getResizedPreview()now reads through a temporary file with a"FORMAT:path"prefix rather than a bare blob read -SVG.phpandOffice.php(the only other two Imagick read paths incore) get the same treatment.The pin has to be path-based, not
setFormat()+readImageBlob(). I initially implemented it that way (matching the literal shape suggested in the OC10-164 discussion), and it does reliably reject a mismatched format - but for every coder pinned here it also silently skips the actual rasterization step:setImageFormat('png')ends up with no effect andgetImageBlob()returns the original, undecoded source bytes. Confirmed for all eightBitmapsubclasses, not just the lazily-rendered ones (fonts, HEIC) where I first suspected it. Path-based reads (new Imagick('FORMAT:' . $path)/readImage('FORMAT:' . $path)) don't have this problem and decode correctly for all of them.Known, accepted residual gap. PDF/Postscript/Illustrator are the Ghostscript-backed providers PostScript-ish content is not foreign to, and Ghostscript does not respect the pin the way the other coders do - so pinning doesn't close cross-coder confusion for those three specifically. This is bounded to their own domain (feeding PostScript to the PDF/Postscript/Illustrator providers isn't "foreign", it's their whole purpose) and mitigated separately by the ImageMagick policy shipped in owncloud-docker/php#309, which denies the MSL/MVG/MSVG coders regardless of entry point.
Font's failure mode for foreign content is a safe blank placeholder, not a hard reject. FreeType fails to parse non-font bytes and Imagick returns an empty canvas without ever reaching a script coder or delegate - so it has its own, separately-worded test rather than sharing the
assertFalse()used for SGI/Photoshop/TIFF/Heic.Adds
tests/data/testimage.{ai,heic,psd,sgi,tiff,ttf}- genuine samples of each format, since none existed before this PR.Sequencing
Per the discussion on OC10-164: independent follow-up, not a blocker for #41827. The 10.16 backport (#41828) stays unmerged until this approach is agreed here first - no changes made there in this PR.
Test plan
make test-php-stylemake test-php-unit TEST_PHP_SUITE=tests/lib/Preview/- 72 tests, 226 assertions, 9 skipped (unrelated: no Movie/Office provider in this environment, plus one HEIF-coder-specific skip - see note below), 0 failuresCoderPinningTest: legitimate content still decodes for all 8Bitmapsubclasses (real fixtures, not synthetic), and PostScript-shaped content is rejected when fed to a non-Ghostscript-backed providerSanitizeTest,SVGSanitizeTest,SVGTest,PDFTest,BitmapTestall still passOfficeTestis skipped in my environment (no LibreOffice binary) - verified the'PDF:' . $path . '[0]'syntax directly against a real PDF instead (subimage selector + format prefix combination Imagick handles correctly), but the actual LibreOffice-generated-PDF path is unverified by an automated test hereHEIFcoder fromHEIC- confirmed separately that the realowncloud/serverimage (built on Ubuntu +libheif1) registers both as distinct coders (identify -list format)