fix(backend): add the missing Life guard to PreviewProvider - #9
Open
nocstah wants to merge 1 commit into
Open
Conversation
The P0 concurrency remediation (f8ecfbd) gave FileOperations, SearchWorker and ThumbnailProvider a Life{mutex,alive} guard so a pool worker can never touch `this` after the owner is destroyed. PreviewProvider has the same pattern and was not part of that commit: it still has no guard and no destructor. Both of its async entry points hand a raw `this` to the global pool and then deliver with QMetaObject::invokeMethod(this, ...): requestText() - reads up to maxBytes and runs SyntaxHighlighter::highlight() on the worker, so the window is as wide as a large file's highlight pass. requestAudio() - MediaInfo::extract() on the worker. invokeMethod() dereferences `this` to find its thread, so if the singleton dies while a worker is still in that read (app shutdown, or an xdg-desktop-portal picker answering and tearing the QML engine down), the worker reads freed memory. Symbolized: #0 QObject::thread() Percius04#1 QMetaObject::invokeMethodImpl Percius04#2 PreviewProvider::requestText(QString const&, int)::{lambda()Percius04#1} Percius04#3 QRunnable::QGenericRunnable::Helper<...>::impl Reproduced against f8ecfbd by firing 8 concurrent requestText() calls on a 560 KB highlightable file, destroying the provider mid-flight and poisoning the freed block: 3/3 runs SIGSEGV at the frame above, 0/3 with this change. (AddressSanitizer would be the better instrument, as used for f8ecfbd; libasan was not available on this machine, hence the heap-poisoning harness, which reproduces the same frame seen in a production coredump.) The fix is f8ecfbd's own pattern, unchanged: Life{mutex,alive} held in a shared_ptr that outlives the singleton, a destructor that marks it dead under the lock, and delivery that takes that same lock before touching `this`. The generation counters need no move here -- unlike SearchWorker's walk, they are only read inside the delivered lambda, on the object's own thread.
nocstah
force-pushed
the
fix/previewprovider-lifetime-guard
branch
from
August 18, 2026 04:08
7c86a8e to
02552e2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
f8ecfbdgave FileOperations, SearchWorker and ThumbnailProvider aLife{mutex,alive}guard so a pool worker can never touchthisafter the owner is destroyed.PreviewProviderhas the same pattern and wasn't in that commit — it still has no guard and no destructor.Both of its async entry points hand a raw
thisto the global pool and then deliver withQMetaObject::invokeMethod(this, ...):requestText()— reads up tomaxBytesand runsSyntaxHighlighter::highlight()on the worker, so the window is as wide as a large file's highlight passrequestAudio()—MediaInfo::extract()on the workerinvokeMethod()dereferencesthisto find its thread, so if the singleton dies while a worker is still in that read — app shutdown, or anxdg-desktop-portalpicker answering and tearing the QML engine down — the worker reads freed memory.Reproduction
Against
f8ecfbd: 8 concurrentrequestText()calls on a 560 KB highlightable file, provider destroyed mid-flight, freed block poisoned so a dangling read faults hard rather than silently reading stale-but-mapped memory.3/3 runs SIGSEGV before, 0/3 after, at this frame (symbolized):
AddressSanitizer is the better instrument here and is what you used for
f8ecfbd;libasanwasn't available on my machine, hence the heap-poisoning harness. It reproduces the same frame I first saw in a real coredump — mine came from the FileChooser portal path (ThumbnailProvider, pre-f8ecfbd), which is what sent me looking at the sibling classes in the first place.The change
Your own pattern from
f8ecfbd, unchanged:Life{mutex,alive}in ashared_ptrthat outlives the singleton, a destructor that marks it dead under the lock, and delivery that takes that same lock before touchingthis.One deliberate difference from
SearchWorker: the generation counters stay as plain members. They're only read inside the delivered lambda, on the object's own thread — unlikeSearchWorker's directory walk, which polls its counter per entry from the worker and therefore genuinely needed theshared_ptrmove.Two files, +38/−2, no behaviour change on the happy path.