From 684952a710cdf2e220a25b32f2d688e508de505f Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Thu, 20 Aug 2026 10:56:50 -0500 Subject: [PATCH] Fix dangling reference in scanner event proxy and missing transaction in share artwork - main.cpp: proxyScannerEventsToApplication() captured its local `postAll` lambda by reference ([&]) inside connections registered on the scanner service's long-lived signals. Those connections outlive the function call, so every scan event since startup was invoking a dangling reference to an already-popped stack frame - undefined behavior, deep in Wt's own signal dispatch, from the very first scan onward. This lines up with what we're seeing in production: sporadic SIGSEGV crashes (exit code 139) inside Wt::Signals::Impl::SignalLinkBase::isConnected(), confirmed via host dmesg, roughly every 1-2 hours of uptime. Fixed by capturing `postAll` by value (it's a stateless lambda, so this is free); `server` remains a valid reference since it lives for the whole program. - PublicShareResource.cpp: the /artwork branch called IArtworkService::getImage(), which lazily resolves artwork through the DB, outside of any transaction (the read transaction that populated `items` had already closed). Confirmed in production logs: "Uncaught exception from handleRequest (aborting request): Operation requires an active transaction". Wrapped it in a read transaction, matching the pattern used elsewhere in this file. --- src/lms/PublicShareResource.cpp | 5 +++++ src/lms/main.cpp | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lms/PublicShareResource.cpp b/src/lms/PublicShareResource.cpp index 8084991e..337506fa 100644 --- a/src/lms/PublicShareResource.cpp +++ b/src/lms/PublicShareResource.cpp @@ -186,6 +186,11 @@ namespace lms if (parts.size() != 3) { response.setStatus(404); return; } const auto item{ std::find_if(items.begin(), items.end(), [&](const auto& value) { return value.id == parts[2]; }) }; if (item == items.end()) { response.setStatus(404); return; } + // IArtworkService::getImage() resolves the artwork through the DB (Wt::Dbo + // lazy-loads on access), so this needs an active transaction on this thread's + // session, same as the block above that populated `items`. Without it, this + // throws "Operation requires an active transaction" (seen in production logs). + auto transaction = _db.getTLSSession().createReadTransaction(); auto artworkService{ core::Service::get() }; auto image{ item->artworkId.isValid() ? artworkService->getImage(item->artworkId, 256) : nullptr }; if (!image) image = artworkService->getDefaultReleaseArtwork(); diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 67614d60..c6021ada 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -241,6 +241,12 @@ namespace lms void proxyScannerEventsToApplication(scanner::IScannerService& scanner, Wt::WServer& server) { + // NOTE: these connections are stored on `scanner`, which outlives this function call + // (it's a core::Service owned by main()'s scope), so nothing here may capture a + // reference to a local of this function. `postAll` used to be captured via `[&]`, + // which left every connected slot holding a dangling reference to this function's + // (long-since-popped) stack frame for the remainder of the program's lifetime. + // Capture it by value instead; it's a stateless lambda, so this is free. auto postAll{ [](Wt::WServer& server, std::function cb) { server.postAll([cb = std::move(cb)] { // may be nullptr, see https://redmine.webtoolkit.eu/issues/8202 @@ -249,35 +255,35 @@ namespace lms }); } }; - scanner.getEvents().scanAborted.connect([&] { + scanner.getEvents().scanAborted.connect([postAll, &server] { postAll(server, [] { LmsApp->getScannerEvents().scanAborted.emit(); LmsApp->triggerUpdate(); }); }); - scanner.getEvents().scanStarted.connect([&] { + scanner.getEvents().scanStarted.connect([postAll, &server] { postAll(server, [] { LmsApp->getScannerEvents().scanStarted.emit(); LmsApp->triggerUpdate(); }); }); - scanner.getEvents().scanComplete.connect([&](const scanner::ScanStats& stats) { + scanner.getEvents().scanComplete.connect([postAll, &server](const scanner::ScanStats& stats) { postAll(server, [=] { LmsApp->getScannerEvents().scanComplete.emit(stats); LmsApp->triggerUpdate(); }); }); - scanner.getEvents().scanInProgress.connect([&](const scanner::ScanStepStats& stats) { + scanner.getEvents().scanInProgress.connect([postAll, &server](const scanner::ScanStepStats& stats) { postAll(server, [=] { LmsApp->getScannerEvents().scanInProgress.emit(stats); LmsApp->triggerUpdate(); }); }); - scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) { + scanner.getEvents().scanScheduled.connect([postAll, &server](const Wt::WDateTime dateTime) { postAll(server, [=] { LmsApp->getScannerEvents().scanScheduled.emit(dateTime); LmsApp->triggerUpdate();