#5195·libvips

fail_on is ignored by thumbnail_buffer and thumbnail_source

Author: zfLQ2qx2Created Aug 25, 2026Updated Sep 3, 2026

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

c
/* 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 image

The 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 master too: the buffer and source variants there contain no reference to fail_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).