-
Notifications
You must be signed in to change notification settings - Fork 301
Open
Description
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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels