Skip to content

Make libppd safe to use from several threads - #103

Merged
tillkamppeter merged 3 commits into
OpenPrinting:masterfrom
alepacheco:raster-error-per-thread
Nov 3, 2026
Merged

tillkamppeter merged 3 commits into
OpenPrinting:masterfrom
alepacheco:raster-error-per-thread

Conversation

@alepacheco

Copy link
Copy Markdown
Contributor

Fixes #100.

libppd is used from several threads at once by Printer Applications built on pappl-retrofit, but it is built without its thread support and keeps some state process-wide. Three commits:

  1. Build with POSIX threads so the library's locks and per-thread data work. configure checks for <pthread.h> (defining HAVE_PTHREAD_H) and for the library providing pthread_key_create(). The existing mutexes in ppd/string.c and ppd/libcups2.c then lock, and ppdGlobals() (with it ppdLastError() and ppdSetConformance()) is per thread, as in CUPS. Single-threaded programs see no change.
  2. Keep the raster error buffer per thread. ppd/raster-error.c keeps each thread's buffer behind a thread key, as ppdGlobals() does (a plain static buffer when built without pthreads), allocated on first use and freed when the thread exits. It is NULL-safe, so ppdRasterExecPS() may report an error before any ppdRasterInterpretPPD() call.
  3. Swap the PPD cache status message under a lock. set_error() in ppd/ppd-cache.c builds the new message first, swaps it in under a mutex and frees the previous one afterwards.

The new test ppd/test_ppd_thread.c, run by make check, checks ppdLastError() per thread, runs 8 threads x 2000 loops of the string pool, of ppdRasterInterpretPPD() / ppdRasterExecPS() with their exact error messages, and of failing ppdCacheCreateWithFile() calls, and calls ppdRasterExecPS() before any other raster function. On master it fails (and crashes in a plain GCC -O2 build); with these commits every check passes in 3 of 3 runs under ThreadSanitizer (no warnings), AddressSanitizer with UndefinedBehaviorSanitizer, and a plain GCC -O2 build, and make check passes.

thread-private.h only maps _ppdMutexLock()/_ppdMutexUnlock() and the
thread-local storage macros onto pthreads when HAVE_PTHREAD_H is defined,
but configure never checked for <pthread.h>, so every build used the
"no threading" fallbacks: the mutexes guarding the string pool (string.c)
and the message catalogs (libcups2.c) did nothing, and ppdGlobals()
(ppd.c) returned one structure shared by all threads.  Printer
applications that call libppd from several threads at once (one per job
or printer, as pappl-retrofit does) thus raced on the string pool and saw
each other's ppdLastError().

Check for <pthread.h> (defining HAVE_PTHREAD_H) and for the library that
provides pthread_key_create().  ppdGlobals(), and with it ppdLastError()
and ppdSetConformance(), is now per thread as in CUPS; single-threaded
programs see no change.

Add test_ppd_thread, which checks that ppdLastError() is kept per thread
and that concurrent use of the string pool keeps its references right.
_ppdRasterAddError(), _ppdRasterClearError() and _ppdRasterErrorString()
shared one static buffer (CUPS keeps it per thread), so concurrent
ppdRasterInterpretPPD() and ppdRasterExecPS() calls, which clear the
buffer and append errors to it, raced: one thread could realloc() the
buffer while another copied a message into it, or append past its end
after the other moved the write position.  The buffer was also only
allocated by _ppdRasterClearError(), so ppdRasterExecPS() reporting an
error before any ppdRasterInterpretPPD() call dereferenced a NULL
pointer.

Allocate the buffer of each thread on first use and free it when the
thread exits, with a thread key as ppdGlobals() uses (a plain static
buffer when built without pthreads).

Extend test_ppd_thread with the raster error checks.
set_error() freed ppd_cache_status_message and then stored the new
message, so two threads failing in ppdCacheCreateWithFile() or
ppdCacheWriteFile() at the same time could both free the same pooled
string.  Build the new message first, swap it in under a mutex and free
the previous one afterwards.

Extend test_ppd_thread with concurrent ppdCacheCreateWithFile() failures.
@tillkamppeter
tillkamppeter merged commit 27c6a35 into OpenPrinting:master Nov 3, 2026
15 checks passed
@tillkamppeter

Copy link
Copy Markdown
Member

Thanks a lot for this PR.

When I copied all the PPD-related functions from libcups into libppd, I completely overlooked the thread support and so I did not overtake the appropriate check into configure.ac, leaving libppd always building without thread support. And you even found that the thread support had some bugs and you have fixed them. Great. Could you fix these bugs also in the original libcups (2.4.x and 2.5.x/master in the "cups" repo). Thanks.

@alepacheco

Copy link
Copy Markdown
Contributor Author

Thanks for merging!

I checked the CUPS repo on both 2.4.x and master, and none of the three bugs is there:

  • Threads compiled out: CUPS's config-scripts/cups-threads.m4 checks for pthread.h, defines HAVE_PTHREAD_H and links the pthread library, so its mutexes (such as the string pool lock in cups/string.c) really lock. Only libppd's configure.ac was missing that check.
  • Raster error buffer: in CUPS, _cupsRasterAddError(), _cupsRasterClearError() and _cupsRasterErrorString() (cupsRasterGetErrorString() on master) keep the buffer in the per-thread globals (_cupsGlobals()->raster_error), and that code handles a buffer that was never allocated. The shared buffer is only in libppd's raster-error.c.
  • PPD cache status message: CUPS's ppd-cache.c reports errors through _cupsSetError(), which is per thread too. The shared ppd_cache_status_message string and set_error() exist only in libppd.

So CUPS doesn't need these fixes. The bugs came from how libppd adapted the code when it was copied out of libcups.

@tillkamppeter

Copy link
Copy Markdown
Member

OK, so it seems that due to my overlooking of thread-readinessof the code I have filled in missing functions with very simple, non-thread-ready ones.

Thank you for checking CUPS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libppd is not thread-safe: built without pthreads, and the raster error buffer and PPD cache status message are shared by all threads

2 participants