#103

No limit on substitution growth: TestGSUBThree.ttf grows "lol" until the process is killed

Opendimenusopened 18d ago

Tested with commit cc63806 (v2.28e), Apple clang 21 -O2, macOS arm64.

kb has no limit on how much a font's substitutions can grow a run. HarfBuzz's test font TestGSUBThree.ttf has a GSUB that keeps expanding the input "lol". HarfBuzz's test text-rendering-tests/tests/GSUB-3.tests only checks that the shaper ends. HarfBuzz ends because it stops a buffer at HB_BUFFER_MAX_LEN_FACTOR (256) glyphs per input character, with a minimum of HB_BUFFER_MAX_LEN_MIN (65536), from src/hb-limits.hh.

Warning: this repro uses many GB of memory.

// cc -O2 runaway.c -o runaway && ./runaway TestGSUBThree.ttf
#define KB_TEXT_SHAPE_IMPLEMENTATION
#include "kb_text_shape.h"
#include "common.h"
#include <sys/resource.h>
#include <time.h>

int main(int argc, char **argv)
{
    kbts_shape_context *context = kbts_CreateShapeContext(0, 0);
    if (!kbts_ShapePushFontFromFile(context, argv[1], 0)) { printf("font did not load\n"); return 1; }

    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);
    kbts_ShapeBegin(context, KBTS_DIRECTION_DONT_KNOW, KBTS_LANGUAGE_DONT_KNOW);
    kbts_ShapeUtf8(context, "lol", 3, KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
    kbts_ShapeEnd(context);

    long glyphs = 0;
    kbts_run run;
    while (kbts_ShapeRun(context, &run)) {
        kbts_glyph *glyph;
        while (kbts_GlyphIteratorNext(&run.Glyphs, &glyph)) { glyphs++; }
    }
    clock_gettime(CLOCK_MONOTONIC, &end);

    struct rusage usage;
    getrusage(RUSAGE_SELF, &usage);
    printf("glyphs: %ld, error: %d, seconds: %.1f, peak RSS: %.1f MB\n", glyphs, (int)kbts_ShapeError(context),
           (double)(end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9,
           usage.ru_maxrss / (1024.0 * 1024.0)); // macOS reports bytes
    return 0;
}

The program never prints its line. On a 48 GB Apple M5 Pro Mac I sampled the process (ps -o rss) and it went like this:

t=0s    rss=33MB
t=30s   rss=6163MB
t=60s   rss=3252MB   (I think RSS falls because macOS compresses memory)
t=90s   rss=1511MB
ended after 125s: SIGKILL

The SIGKILL did not come from the program or my script. A first run of the same program also ended with SIGKILL, after about two minutes. Using the direct API in another program, I measured a peak RSS of 10.6 GB in 137 seconds before it failed.

So a single font file can make shaping use as much memory and time as the machine has. Limiting memory with a custom allocator does not help today, because of the NULL handling I describe in #65.

A cap on glyphs per input character, like HarfBuzz's, would make this end, maybe reported as KBTS_SHAPE_ERROR_OUT_OF_MEMORY or a new error.

(I found this with help from Claude, with a harness that runs HarfBuzz's shaping test suite through kb and through HarfBuzz and compares them. I checked the repro by hand.)

Activity

0

No activity yet.