Repository navigation
Make libppd safe to use from several threads - #103
Conversation
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.
|
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 |
|
Thanks for merging! I checked the CUPS repo on both 2.4.x and master, and none of the three bugs is there:
So CUPS doesn't need these fixes. The bugs came from how libppd adapted the code when it was copied out of libcups. |
|
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. |
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:
<pthread.h>(definingHAVE_PTHREAD_H) and for the library providingpthread_key_create(). The existing mutexes inppd/string.candppd/libcups2.cthen lock, andppdGlobals()(with itppdLastError()andppdSetConformance()) is per thread, as in CUPS. Single-threaded programs see no change.ppd/raster-error.ckeeps each thread's buffer behind a thread key, asppdGlobals()does (a plain static buffer when built without pthreads), allocated on first use and freed when the thread exits. It is NULL-safe, soppdRasterExecPS()may report an error before anyppdRasterInterpretPPD()call.set_error()inppd/ppd-cache.cbuilds 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 bymake check, checksppdLastError()per thread, runs 8 threads x 2000 loops of the string pool, ofppdRasterInterpretPPD()/ppdRasterExecPS()with their exact error messages, and of failingppdCacheCreateWithFile()calls, and callsppdRasterExecPS()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, andmake checkpasses.