preview_data: guard the pipe-nodes walk in dt_preview_data_is_fresh with busy_mutex - #22119
Conversation
…ith busy_mutex dt_preview_data_is_fresh() (introduced in 8a26f35 / darktable-org#21397, the shared preview-data service for interactive editing) walks dev->preview_pipe->nodes and hashes the matching piece from the GUI thread while holding only the module's own gui_lock. pipe->nodes is actually protected by pipe->busy_mutex: dt_dev_pixelpipe_cleanup_nodes() frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVE in dt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walks pipe->nodes off the GUI thread without busy_mutex can therefore dereference a piece that gets freed out from under it -- reproduced as a segfault dereferencing a freed piece->module in _dev_pixelpipe_cache_basichash(), from a module's preview-pipe-finished GUI callback calling dt_preview_data_is_fresh(). colorequal.c's own interactive hue-editing mode (also from darktable-org#21397) calls dt_preview_data_is_fresh() from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice. Take pipe->busy_mutex around the walk. It must be a trylock, not a blocking lock: a module's process() runs under busy_mutex for the whole pipe run (dt_dev_pixelpipe_process) and can itself call dt_preview_data_store() (colorequal.c does, twice), which takes this same module's gui_lock -- the opposite order -- so a blocking lock here would AB-BA deadlock against it. A pipe that's currently busy can't have fresh data for us anyway, so treating "busy" as "not fresh" costs nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU
|
Just so that you get something other than AI slop, I was playing around with a private module when I ran into a rather easy to reproduce segfault. Turned out it's not from my code and this appears to be the fix. Thought that it should be in the official codebase. |
|
I'll let @jenshannoschwalm review and have the final word but to me this looks correct. |
There was a problem hiding this comment.
🟢 Approval recommended
The change correctly matches the pixelpipe’s locking contract (protecting pipe->nodes with busy_mutex) while using trylock to avoid the documented lock-order deadlock risk.
Pull request overview
This PR fixes a concurrency hazard in the preview-data freshness check by ensuring dt_preview_data_is_fresh() only walks preview_pipe->nodes while holding the pixelpipe’s busy_mutex, preventing use-after-free during concurrent pipe topology rebuilds.
Changes:
- Acquire
pipe->busy_mutexvia trylock around thepipe->nodeswalk and subsequent hash computation. - Treat a busy pipe (trylock fails) as “not fresh” to avoid blocking and potential lock-order deadlocks with
gui_lock.
File summaries
| File | Description |
|---|---|
src/develop/preview_data.c |
Wraps the preview-pipe node traversal/hash in a busy_mutex trylock to prevent races with node cleanup/rebuild. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Yes, correct to me too. |
Summary
dt_preview_data_is_fresh()(introduced in 8a26f35 / #21397, the shared preview-data service for interactive editing) walksdev->preview_pipe->nodesand hashes the matching piece from the GUI thread while holding only the module's owngui_lock.pipe->nodesis actually protected bypipe->busy_mutex:dt_dev_pixelpipe_cleanup_nodes()frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVEindt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walkspipe->nodesoff the GUI thread withoutbusy_mutexcan therefore dereference a piece that gets freed out from under it.Root-caused from a real crash (SIGSEGV dereferencing a freed
piece->modulein_dev_pixelpipe_cache_basichash(), backtrace attached below) hit by an out-of-tree module whose preview-pipe-finished GUI callback callsdt_preview_data_is_fresh().colorequal.c's own interactive hue-editing mode (also from #21397) callsdt_preview_data_is_fresh()from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice.pipe->busy_mutexaround the walk.process()runs underbusy_mutexfor the whole pipe run (dt_dev_pixelpipe_process) and can itself calldt_preview_data_store()(colorequal.cdoes, twice), which takes this same module'sgui_lock— the opposite order — so a blocking lock here would AB-BA deadlock against it.Crash backtrace (relevant frames)
Test plan
cmake --build build --target darktable— clean build, no new warnings, on top of currentmaster(fd59685185)process(), holdingbusy_mutexvia the pipe-run wrapper, blocked on this module'sgui_lock) to confirm the trylock is required and correct, not just a plausible-looking fixcolorequal/toneequal's existing interactive-mode paths🤖 Generated with Claude Code
https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU