vips_sink_screen: crash, background render thread can use a freed Render
Bug report
Description
vips_sink_screen() can crash: the background render thread can pick up a
Render whose reference count has already reached zero, and then use it while
the thread that dropped the last reference is freeing it.
render_dirty_all is a worklist of raw Render * pointers which holds no
reference of its own. render_dirty_get() takes the reference when it pulls one
off, and render_free() unlinks — both under render_dirty_lock. But
render_unref() drops the count outside that lock:
:315 kill = g_atomic_ref_count_dec(&render->ref_count); /* not under the lock */
:325 if (kill) render_free(render); /* takes it only at :253 */Between :315 and :253 the render has reached zero but is still on the worklist:
T_user T_render (background render thread)
────────────────────────────────────────── ────────────────────────────────────
render_close_cb() -> render_unref()
:315 dec -> 1->0, kill = TRUE
:325 render_free()
:253 g_mutex_lock(&render_dirty_lock)
--- blocks: the render thread holds it ---
render_dirty_get()
:1017 g_mutex_lock(&render_dirty_lock)
:1023 head of render_dirty_all
(still linked)
:1028 render_ref() <-- 0 -> 1
:1030 unlink
:1033 unlock
--- acquires the lock ---
:254 g_slist_find() -> no-op, already gone
:278 VIPS_UNREF(render->in)
:280 g_free(render)
vips_threadpool_run(render->in, ...)
^ freedThe window is not a few instructions wide: T_user blocks on the lock at :253,
so it stays open for as long as the render thread holds render_dirty_lock.
This needs notify_fn != NULL. tile_queue() only calls render_dirty_put()
on the if (render->notify) branch; with a NULL notify the tile is painted
synchronously and the render never reaches the worklist.
Reproducible example
Self-contained, no image files, no sanitizer needed.
/* gcc -g -o mre mre.c `pkg-config vips --cflags --libs` */
#include <stdio.h>
#include <glib.h>
#include <vips/vips.h>
/* A non-NULL notify is what selects the asynchronous path.
*/
static void
notify_cb(VipsImage *im, VipsRect *rect, void *a)
{
}
int
main(int argc, char **argv)
{
if (VIPS_INIT(argv[0]))
vips_error_exit(NULL);
for (int i = 0; i < 4000; i++) {
VipsImage *in, *out;
VipsRegion *region;
VipsRect r = { 0, 0, 64, 64 };
if (vips_gaussnoise(&in, 2048, 2048, NULL))
vips_error_exit(NULL);
out = vips_image_new();
if (vips_sink_screen(in, out, NULL, 64, 64, 32, 0, notify_cb, NULL))
vips_error_exit(NULL);
/* Dirty a tile: this queues the render for the background thread.
*/
if ((region = vips_region_new(out))) {
(void) vips_region_prepare(region, &r);
g_object_unref(region);
}
/* ... and drop it again straight away.
*/
g_object_unref(out);
g_object_unref(in);
}
printf("survived\n");
return 0;
}for i in $(seq 20); do ./mre >/dev/null 2>&1 || echo crash; doneExpected result
All 20 runs print survived.
Actual result
Most runs die — 13 and 16 out of 20 in two batches here — almost all with
SIGSEGV, occasionally with SIGABRT and glibc reporting
corrupted double-linked list.
Environment
- OS: Linux x86_64, glibc 2.39, glib 2.80.0, gcc 13
- libvips version: master
8805627b,meson setup build -Doptimization=2 -Ddebug=false - 8.18.5 carries the identical
render_unref()and the same raw-pointer worklist, so it looks affected too.
Source: libvips/libvips