java-topology/defects/ffmpeg/patch/ffmpeg-0002-gif-shrink-palette-hashset.md

6.3 KiB
Raw Blame History

UNDF: UNDF-2026-000000684

UNDF: (pending)

ffmpeg-0002: shrink_palette — O(P²) palette dedup via linear scan

CWE-407 — Algorithmic Complexity

Field Value
ID ffmpeg-0002
Severity MEDIUM
Ecosystem FFmpeg
File libavcodec/gif.c
Lines 6787
Complexity O(P²) where P = AVPALETTE_COUNT = 256
Hot path Called once per GIF frame when a local palette is used

Defect

shrink_palette() deduplicates palette entries by comparing each entry against all previously-seen entries via a nested linear scan:

static void shrink_palette(const uint32_t *src, uint8_t *map,
                           uint32_t *dst, size_t *palette_count)
{
    size_t colors_seen = 0;

    for (size_t i = 0; i < AVPALETTE_COUNT; i++) {
        int seen = 0;
        for (size_t c = 0; c < colors_seen; c++) {    /* O(P) inner scan */
            if (src[i] == dst[c]) {
                seen = 1;
                break;
            }
        }
        if (!seen) {
            dst[colors_seen] = src[i];
            map[i] = colors_seen;
            colors_seen++;
        }
    }

    *palette_count = colors_seen;
}

AVPALETTE_COUNT is 256. In the worst case (all 256 entries are unique) the inner loop executes 0+1+2+…+255 = 32,640 comparisons — O(P²).

This function is called from gif_encode_frame() (line 364) on every frame that uses a local palette. A GIF animation at 30 fps with local palettes executes 30 × 32,640 = ~979,200 comparisons per second just for palette dedup.

Fix

Replace the growing-dst linear scan with a boolean seen[AVPALETTE_COUNT] lookup table. Since palette indices are 0255, a direct-index array gives O(P) total work:

static void shrink_palette(const uint32_t *src, uint8_t *map,
                           uint32_t *dst, size_t *palette_count)
{
    /* CWE-407 fix: O(P) dedup using value-indexed lookup instead of O(P²)
     * linear scan.  We cannot use a hash on 32-bit ARGB values directly
     * because palette size P=256 is small, but multiple src entries can share
     * the same 32-bit value.  A simple seen[] array indexed by src position
     * does not help; instead, build an index mapping each 32-bit colour to
     * its first dst slot using a hash table over the 256-entry colour space. */
    uint8_t color_to_slot[1 << 16];   /* 64 KiB; keyed on lower 16 bits */
    memset(color_to_slot, 0xFF, sizeof(color_to_slot)); /* 0xFF = not seen */
    size_t colors_seen = 0;

    for (size_t i = 0; i < AVPALETTE_COUNT; i++) {
        uint32_t color = src[i];
        uint16_t key   = (uint16_t)(color ^ (color >> 16)); /* fold to 16 bits */
        uint8_t  slot  = color_to_slot[key];

        if (slot == 0xFF || dst[slot] != color) {
            /* Either not seen or hash collision — fall back to linear scan
             * of already-written entries (collision rate is negligible for
             * P=256). */
            int found = 0;
            for (size_t c = 0; c < colors_seen; c++) {
                if (dst[c] == color) {
                    map[i] = (uint8_t)c;
                    found = 1;
                    break;
                }
            }
            if (!found) {
                color_to_slot[key] = (uint8_t)colors_seen;
                dst[colors_seen]   = color;
                map[i]             = (uint8_t)colors_seen;
                colors_seen++;
            }
        } else {
            map[i] = slot;
        }
    }

    *palette_count = colors_seen;
}

A simpler, fully correct O(P) alternative (preferred for readability):

static void shrink_palette(const uint32_t *src, uint8_t *map,
                           uint32_t *dst, size_t *palette_count)
{
    /* CWE-407 fix: use a 256-slot seen-index array keyed by source position.
     * Since AVPALETTE_COUNT == 256 and values can repeat, we need a
     * value→slot map.  With only 256 possible slots the correct O(P) approach
     * is to keep a reverse map dst_index[color] using a hash map or, simpler,
     * a flat map from src index to dst slot built in one pass. */
    uint8_t dst_slot[AVPALETTE_COUNT]; /* dst slot for each src[i] if seen */
    uint8_t in_dst[AVPALETTE_COUNT];   /* has src[i] colour been placed? */
    memset(in_dst, 0, sizeof(in_dst));
    size_t colors_seen = 0;

    /* First pass: assign dst slots, record which src indices map where.
     * Use a separate deduplicated reverse table keyed on src index. */

    /* Simplest correct O(P) fix: replace the inner scan with a hash set
     * over 32-bit colour values.  P=256 so a flat array of 256 buckets
     * (robin-hood or linear-probe) is ideal.  The implementation below
     * uses C99 designated initialisers for a compact open-address table. */

    /* For production use av_ts_make_time_string / uthash or a 64KB LUT.
     * The patch below is illustrative; the key invariant is that no O(P²)
     * loop exists. */

    uint32_t seen_color[AVPALETTE_COUNT];
    uint8_t  seen_slot[AVPALETTE_COUNT];
    memset(seen_color, 0xFF, sizeof(seen_color)); /* sentinel: 0xFFFFFFFF */

    for (size_t i = 0; i < AVPALETTE_COUNT; i++) {
        uint32_t color = src[i];
        size_t   h     = (color * 2654435761UL) >> 24; /* Knuth hash → [0,255] */
        /* Linear-probe open-address lookup */
        while (seen_color[h] != 0xFFFFFFFF && seen_color[h] != color)
            h = (h + 1) & 0xFF;
        if (seen_color[h] == color) {
            map[i] = seen_slot[h];
        } else {
            seen_color[h] = color;
            seen_slot[h]  = (uint8_t)colors_seen;
            dst[colors_seen] = color;
            map[i]           = (uint8_t)colors_seen;
            colors_seen++;
        }
    }

    *palette_count = colors_seen;
}

Speedup

Frames Before (comparisons) After (comparisons) Ratio
1 frame, P=256 all unique 32,640 256 hash ops ~127×
30 fps × 1 s 979,200 7,680 ~127×

Typical GIF palettes have 64256 entries; speedup is proportional to the number of unique colours seen.

Notes

The sentinel value 0xFFFFFFFF is valid as a BGRA colour (fully opaque white in some encodings). A production patch should use a separate boolean occupied[256] table rather than the sentinel, or use av_memdup + uthash. The key invariant — eliminating the O(P²) inner loop — is the essential fix.