Skip to content

Font cache is not thread safe when using many different font sizes #475

@ttenbrinkzenz

Description

@ttenbrinkzenz

Reproducer in C++17

// CImg draw_text() thread-safety crash reproducer
//
// Root cause: CImg::font() acquires mutex(11), builds a font in a static
// CImgList fonts[16] cache at slot 'ind', releases the mutex, then returns
// a *reference* into fonts[ind]. A concurrent thread can evict that slot via
// memmove+memset while the first thread still holds the reference, causing a
// crash inside get_resize() / _draw_text().
//
// Confirmed on CImg v3.0.3 and v3.7.0 with C++17. Reproducer was tested on Windows
// but originaly this happened in a Linux production environment.

#include <atomic>
#include <csignal>
#include <cstdio>
#include <thread>
#include <vector>

#ifdef _WIN32
#  include <windows.h>
#  include <crtdbg.h>
#endif

#define cimg_display  0
#define cimg_verbosity 0
#include "CImg.h"
using namespace cimg_library;

static void crash_handler(int sig) {
    fprintf(stderr, "\nCRASH: signal %d (%s) -- CImg font() cache race confirmed.\n",
            sig, sig == SIGABRT ? "SIGABRT" : sig == SIGSEGV ? "SIGSEGV" : "?");
    fflush(stderr);
    _exit(1);
}

static std::atomic<bool> go{false};

static void worker(int id) {
    while (!go.load(std::memory_order_acquire)) {}
    for (int i = 0; ; ++i) {
        // 50 distinct sizes forces cache eviction (cache holds only 16 slots)
        unsigned int font_size = 13 + static_cast<unsigned int>(id * 7 + i) % 50;
        CImg<unsigned char> img;
        const unsigned char color = 1;
        img.draw_text(0, 0, "Test", &color, 0, 1.f, font_size);
        if (img.width() < 0) fputc('.', stdout); // prevent elision
    }
}

int main() {
#ifdef _WIN32
    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
    _CrtSetReportMode(_CRT_ERROR,  _CRTDBG_MODE_FILE);
    _CrtSetReportFile(_CRT_ERROR,  _CRTDBG_FILE_STDERR);
    SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
#endif
    signal(SIGABRT, crash_handler);
    signal(SIGSEGV, crash_handler);
    cimg::exception_mode(0);

    printf("CImg v%u: 8 threads calling draw_text() concurrently with 50 font sizes.\n"
           "Expected: SIGABRT/SIGSEGV crash within seconds.\n\n", cimg_version);
    fflush(stdout);

    std::vector<std::thread> threads;
    for (int i = 0; i < 8; ++i)
        threads.emplace_back(worker, i);
    go.store(true, std::memory_order_release);
    for (auto& t : threads) t.join();

    printf("No crash.\n");
    return 0;
}

Please let me know if there is some way to avoid this, but our application uses quite a few different font sizes and we'd rather not have a global lock on every draw_text.

Basically, if I understand it correctly, the font cache returns a reference, but this gets invalidated the moment the cache is full and it moves everything left, causing crashes when this one is used.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions