fail_on is ignored by thumbnail_buffer and thumbnail_source
Bug report
Description
All three thumbnail operations declare a fail_on argument, and it is the same VIPS_ARG_ENUM on the shared VipsThumbnail base class (thumbnail.c:1247). Only the file form forwards it to the loader it opens.
vips_thumbnail_file_get_info() and vips_thumbnail_file_open() pass "fail_on", thumbnail->fail_on to every one of their vips_image_new_from_file() calls (9 sites, thumbnail.c:1310–1420). vips_thumbnail_buffer_get_info() / _open() (1549–1660) and vips_thumbnail_source_get_info() / _open() (1770–1880) pass option_string and "access", VIPS_ACCESS_SEQUENTIAL to their vips_image_new_from_buffer() and vips_image_new_from_source() calls, and never mention fail_on. So for those two the argument is accepted, validated, and then ignored.
The default is VIPS_FAIL_ON_NONE, so the failure mode is quiet: a service that thumbnails an upload it holds in memory, or streams from a socket, asks for fail_on=error, gets no error, and returns a plausible-looking thumbnail of the half of the file that arrived — with a 200. The same code against a temporary file behaves the way the argument reads.
Reproducible example
/* gcc -O2 -o thumb-failon thumb-failon.c $(pkg-config --cflags --libs vips) */
#include <stdio.h>
#include <unistd.h>
#include <vips/vips.h>
static const char *path = "/tmp/truncated.jpg";
static char *jpeg;
static size_t jpeg_len;
/* A JPEG cut off part way through its scan data: the header survives, so every
* case below builds a pipeline, and the damage is only found when the pixels
* are read.
*/
static void
make_truncated(void)
{
VipsImage *im;
char *whole;
size_t whole_len;
if (vips_gaussnoise(&im, 400, 300, NULL) ||
vips_jpegsave_buffer(im, (void **) &whole, &whole_len, NULL))
vips_error_exit(NULL);
g_object_unref(im);
jpeg_len = whole_len * 2 / 3;
jpeg = g_memdup2(whole, jpeg_len);
g_free(whole);
if (!g_file_set_contents(path, jpeg, jpeg_len, NULL))
vips_error_exit("cannot write %s", path);
printf("%zu byte JPEG truncated to %zu bytes\n\n", whole_len, jpeg_len);
}
/* Read the thumbnail's pixels: fail_on is a loader setting, so it can only show
* up when the loader is asked to decode.
*/
static void
try(const char *what, VipsImage *out, int result)
{
double avg;
if (!result)
result = vips_avg(out, &avg, NULL);
if (result) {
char *msg = vips_error_buffer_copy();
char *nl = strchr(msg, '\n');
if (nl)
*nl = '\0';
printf("%-42s REFUSED %s\n", what, msg);
g_free(msg);
}
else
printf("%-42s ACCEPTED %d x %d thumbnail of a broken file\n",
what, vips_image_get_width(out), vips_image_get_height(out));
vips_error_clear();
VIPS_UNREF(out);
}
int
main(int argc, char **argv)
{
VipsImage *out;
VipsSource *source;
if (VIPS_INIT(argv[0]))
vips_error_exit(NULL);
make_truncated();
out = NULL;
try("thumbnail, no fail_on",
out, vips_thumbnail(path, &out, 100, NULL));
out = NULL;
try("thumbnail, fail_on=error",
out, vips_thumbnail(path, &out, 100,
"fail_on", VIPS_FAIL_ON_ERROR, NULL));
out = NULL;
try("thumbnail_buffer, fail_on=error",
out, vips_thumbnail_buffer(jpeg, jpeg_len, &out, 100,
"fail_on", VIPS_FAIL_ON_ERROR, NULL));
out = NULL;
try("thumbnail_buffer, option_string",
out, vips_thumbnail_buffer(jpeg, jpeg_len, &out, 100,
"option_string", "fail_on=error", NULL));
out = NULL;
source = vips_source_new_from_memory(jpeg, jpeg_len);
try("thumbnail_source, fail_on=error",
out, vips_thumbnail_source(source, &out, 100,
"fail_on", VIPS_FAIL_ON_ERROR, NULL));
VIPS_UNREF(source);
out = NULL;
source = vips_source_new_from_memory(jpeg, jpeg_len);
try("thumbnail_source, option_string",
out, vips_thumbnail_source(source, &out, 100,
"option_string", "fail_on=error", NULL));
VIPS_UNREF(source);
g_free(jpeg);
unlink(path);
vips_shutdown();
return 0;
}Expected result
fail_on=error refuses the truncated file whichever thumbnail operation it is given to.
Actual result
47365 byte JPEG truncated to 31576 bytes
thumbnail, no fail_on ACCEPTED 100 x 75 thumbnail of a broken file
thumbnail, fail_on=error REFUSED VipsJpeg: premature end of JPEG image
thumbnail_buffer, fail_on=error ACCEPTED 100 x 75 thumbnail of a broken file
thumbnail_buffer, option_string REFUSED VipsJpeg: premature end of JPEG image
thumbnail_source, fail_on=error ACCEPTED 100 x 75 thumbnail of a broken file
thumbnail_source, option_string REFUSED VipsJpeg: premature end of JPEG imageThe option_string lines are the same request written as a load option string, and they show the loader does honour it — the argument is reaching the operation and not the loader.
Environment
- OS: Linux (Amazon Linux 2023, x86_64)
- libvips version: 8.18.5, built from source, statically linked. Present in
mastertoo: the buffer and source variants there contain no reference tofail_on.
Severity / impact
- Incorrect result.
Additional context
If forwarding it is the intended behaviour, the fix looks mechanical — "fail_on", thumbnail->fail_on, alongside the existing "access", VIPS_ACCESS_SEQUENTIAL, at the 11 buffer and 11 source call sites — and I would be glad to send a PR with a test using a truncated JPEG. I did not want to write one before asking, in case the deliberate design is that these two forms take loader options only through option_string, in which case the fix is presumably the other direction: document it, or drop the argument from those two classes so that setting it fails instead of doing nothing.
Found while writing a Go binding, where this needed working around: the two operations now refuse fail_on and name option_string in the error rather than silently accepting a setting that has no effect (the reasoning).
Source: libvips/libvips