Skip to content

Fix VipsDriver save() corrupting source on same-path writes#326

Merged
freekmurze merged 5 commits into
mainfrom
fix-vips-save-and-add-test-coverage
Apr 29, 2026
Merged

Fix VipsDriver save() corrupting source on same-path writes#326
freekmurze merged 5 commits into
mainfrom
fix-vips-save-and-add-test-coverage

Conversation

@freekmurze

Copy link
Copy Markdown
Member

Summary

  • libvips streams from the source while writing the result. When loadFile() and save() target the same path, libvips reads from the file as it's being overwritten, which can crash libjpeg-turbo with SIGBUS on JPEGs (libvips/libvips#4397). When the destination matches originalPath, write to a sibling temp file and atomically rename over the destination.
  • Add vips to the drivers test dataset so the existing matrix exercises it (previously gd + imagick only, which is why the regression slipped through).
  • Update CI to install libvips and enable FFI so the new dataset entry actually runs.

Background

This was reported via spatie/laravel-medialibrary#3923. Medialibrary's conversion pipeline calls loadFile($path)->...->save() (no path), which now resolves back to the load path due to PR #321. With 'access' => 'sequential' the source is streamed during write, triggering the libvips bug. The reporter sees deterministic SIGBUS on Sail/Docker/WSL2; on macOS the same call silently produces an altered file (different MD5 from the same input).

The fix is local to VipsDriver::save() so every consumer (medialibrary or otherwise) is protected without needing changes downstream.

freekmurze and others added 5 commits April 29, 2026 10:49
… path

libvips streams from the source while writing the result. When loadFile()
and save() target the same path, libvips reads from the file as it's being
overwritten, which can crash libjpeg-turbo with SIGBUS on JPEGs (see
libvips/libvips#4397). When the destination matches originalPath, write
to a sibling temp file and atomically rename over the destination.

Also adds vips to the drivers test dataset so the existing matrix exercises
it, with the CI workflow installing libvips and enabling FFI.
- Throw CouldNotSaveImage on rename or buffer-write failure rather than
  silently corrupting the destination
- Make $originalPath nullable with a default of null instead of relying on
  isset() against an uninitialised typed property
- Drop @Unlink, the surrounding file_exists check already guards it
- Fold the dimension assertion into the existing dataset test and remove
  the duplicate vips-only test (the dataset addition already covers vips)
- Reset quality on loadFile() like GdDriver does, so calling quality()
  on one image doesn't bleed into a later image loaded on the same driver
  instance (this surfaced now that vips is in the shared dataset)
- Drop the CannotOptimizePng throw and instead map quality (0-100) to
  PNG compression (0-9) on save, matching GdDriver's behavior
Vips uses Pango/Cairo for text, which produces a different rendering
from the FreeType-based output the snapshots were captured against.
Generating vips-specific snapshots would need a separate baseline
captured on the same CI environment, which is out of scope here.
@freekmurze freekmurze merged commit 8d39e9a into main Apr 29, 2026
11 checks passed
@freekmurze freekmurze deleted the fix-vips-save-and-add-test-coverage branch April 29, 2026 09:23
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.

1 participant