Skip to content

Commit 41cc31f

Browse files
dkulpclaude
andcommitted
fix(playlist): synchronize the cleanup lists and the status flag
Running the ownership fix under ThreadSanitizer surfaced two pre-existing races the old use-after-free noise had drowned out: - The condemned-playlist and parked-entry lists are reached from command threads as well as the main loop -- a stop on an HTTP thread condemns instances in SetIdle and a Load parks replaced entries -- while the main loop drained both unsynchronized. Every access now holds one lock; deletes still run outside it (entry destructors park further entries, so the entry drain swaps the list out and loops until a pass drains nothing). The lock is held across the whole condemned pass so SetIdle's condemned-parent walk never observes a half-drained list. - m_status was volatile, which never made the unlocked cross-thread getter reads well-defined; it is now atomic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 03d9569 commit 41cc31f

2 files changed

Lines changed: 72 additions & 23 deletions

File tree

src/playlist/Playlist.cpp

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
// still holding a snapshot of survives until that reader is done with it.
6060
// Between push and drain the instance is fully alive, which is what SetIdle's
6161
// "is this parent condemned" walk below relies on.
62+
// Both lists are reached from command threads as well as the main loop --
63+
// SetIdle/SwitchToInsertedPlaylist condemn instances from whatever thread runs
64+
// the transition, and a Load() on a command thread parks replaced entries -- so
65+
// every access holds PL_CLEANUPS_LOCK. Deletes never run under it.
66+
static std::mutex PL_CLEANUPS_LOCK;
6267
static std::list<Playlist*> PL_CLEANUPS;
6368
// Entries deleted while one of their own methods may still be on the
6469
// call stack (e.g. a "Start Playlist" command entry that reloads this
@@ -1135,27 +1140,48 @@ int Playlist::Process(void) {
11351140
// LogExcess(VB_PLAYLIST, "Playlist::Process: %s, section %s, position: %d\n", m_name.c_str(), m_currentSectionStr.c_str(), m_sectionPosition);
11361141

11371142
if (tl_processDepth == 1) {
1138-
if (!PL_CLEANUPS.empty()) {
1139-
PL_CLEANUPS.sort();
1140-
PL_CLEANUPS.unique();
1141-
while (!PL_CLEANUPS.empty()) {
1142-
Playlist* p = PL_CLEANUPS.front();
1143-
// Drops the player's owning reference rather than deleting.
1144-
// If a reader thread still holds a snapshot of p, the instance
1145-
// stays alive until that snapshot releases; its destructor then
1146-
// runs on this thread in a later DrainRetiredPlaylists().
1147-
Player::INSTANCE.ReleasePlaylist(p);
1148-
PL_CLEANUPS.pop_front();
1143+
{
1144+
// Held across the whole condemned pass (not swap-and-release) so
1145+
// SetIdle's condemned-parent walk on another thread sees either the
1146+
// full pre-drain list or the post-release state, never a moment
1147+
// where a condemned instance has left the list but still looks
1148+
// current-able. ReleasePlaylist only nests the owner's registry
1149+
// lock inside; nothing takes these two in the other order.
1150+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1151+
if (!PL_CLEANUPS.empty()) {
1152+
PL_CLEANUPS.sort();
1153+
PL_CLEANUPS.unique();
1154+
while (!PL_CLEANUPS.empty()) {
1155+
Playlist* p = PL_CLEANUPS.front();
1156+
// Drops the player's owning reference rather than deleting.
1157+
// If a reader thread still holds a snapshot of p, the
1158+
// instance stays alive until that snapshot releases; its
1159+
// destructor then runs on this thread in a later
1160+
// DrainRetiredPlaylists().
1161+
Player::INSTANCE.ReleasePlaylist(p);
1162+
PL_CLEANUPS.pop_front();
1163+
}
11491164
}
11501165
}
11511166
// Runs the destructors the shared_ptr deleter deferred — here, between
11521167
// the two lists, because ~Playlist parks its entries on
11531168
// PL_ENTRY_CLEANUPS and those must be freed in the same tick, exactly
1154-
// as when the drain above did the delete itself.
1169+
// as when the drain above did the delete itself. Not under the lock:
1170+
// those destructors push onto PL_ENTRY_CLEANUPS themselves.
11551171
Player::INSTANCE.DrainRetiredPlaylists();
1156-
while (!PL_ENTRY_CLEANUPS.empty()) {
1157-
delete PL_ENTRY_CLEANUPS.front();
1158-
PL_ENTRY_CLEANUPS.pop_front();
1172+
// Entry destructors can park further entries (nested sub-entries), so
1173+
// swap-and-delete until a pass drains nothing.
1174+
while (true) {
1175+
std::list<PlaylistEntryBase*> entries;
1176+
{
1177+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1178+
entries.swap(PL_ENTRY_CLEANUPS);
1179+
}
1180+
if (entries.empty())
1181+
break;
1182+
for (auto* e : entries) {
1183+
delete e;
1184+
}
11591185
}
11601186
}
11611187
std::unique_lock<std::recursive_mutex> lck(m_playlistMutex);
@@ -1431,15 +1457,20 @@ Playlist* Playlist::SwitchToInsertedPlaylist(bool isStopping) {
14311457
Player::INSTANCE.SetCurrentPlaylist(pl);
14321458
if (replaceSelf) {
14331459
m_parent = nullptr;
1460+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
14341461
PL_CLEANUPS.push_back(this);
14351462
}
14361463
return pl;
14371464
}
1438-
PL_CLEANUPS.push_back(pl);
1465+
{
1466+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1467+
PL_CLEANUPS.push_back(pl);
1468+
}
14391469
if (replaceSelf && Player::INSTANCE.PlaylistSnapshot().get() != this) {
14401470
// pl failed to start and its own SetIdle() already handed the
14411471
// player on to our parent, so nothing will process us again.
14421472
m_parent = nullptr;
1473+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
14431474
PL_CLEANUPS.push_back(this);
14441475
}
14451476
}
@@ -1499,8 +1530,11 @@ void Playlist::SetIdle(bool exit) {
14991530
// parent can be condemned while we're exiting when a re-entrant stop tore
15001531
// down an intermediate level of an inserted-playlist stack.
15011532
Playlist* par = m_parent;
1502-
while (par && std::find(PL_CLEANUPS.begin(), PL_CLEANUPS.end(), par) != PL_CLEANUPS.end()) {
1503-
par = par->m_parent;
1533+
{
1534+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1535+
while (par && std::find(PL_CLEANUPS.begin(), PL_CLEANUPS.end(), par) != PL_CLEANUPS.end()) {
1536+
par = par->m_parent;
1537+
}
15041538
}
15051539
if (par && exit) {
15061540
// par is still owned: it is on the parent chain and not condemned, so
@@ -1509,7 +1543,10 @@ void Playlist::SetIdle(bool exit) {
15091543
if (par->getPlaylistStatus() == FPP_STATUS_PLAYLIST_PAUSED) {
15101544
par->Resume();
15111545
}
1512-
PL_CLEANUPS.push_back(this);
1546+
{
1547+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1548+
PL_CLEANUPS.push_back(this);
1549+
}
15131550

15141551
if (par->getPlaylistStatus() != FPP_STATUS_IDLE)
15151552
publishIdle = false;
@@ -1554,19 +1591,28 @@ int Playlist::Cleanup(void) {
15541591
while (m_leadIn.size()) {
15551592
PlaylistEntryBase* entry = m_leadIn.back();
15561593
m_leadIn.pop_back();
1557-
PL_ENTRY_CLEANUPS.push_back(entry);
1594+
{
1595+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1596+
PL_ENTRY_CLEANUPS.push_back(entry);
1597+
}
15581598
}
15591599

15601600
while (m_mainPlaylist.size()) {
15611601
PlaylistEntryBase* entry = m_mainPlaylist.back();
15621602
m_mainPlaylist.pop_back();
1563-
PL_ENTRY_CLEANUPS.push_back(entry);
1603+
{
1604+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1605+
PL_ENTRY_CLEANUPS.push_back(entry);
1606+
}
15641607
}
15651608

15661609
while (m_leadOut.size()) {
15671610
PlaylistEntryBase* entry = m_leadOut.back();
15681611
m_leadOut.pop_back();
1569-
PL_ENTRY_CLEANUPS.push_back(entry);
1612+
{
1613+
std::lock_guard<std::mutex> lk(PL_CLEANUPS_LOCK);
1614+
PL_ENTRY_CLEANUPS.push_back(entry);
1615+
}
15701616
}
15711617
return 1;
15721618
}

src/playlist/Playlist.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ class Playlist {
129129
// one home. Caller must hold m_playlistMutex.
130130
PlaylistEntryBase* CurrentEntry();
131131

132-
volatile PlaylistStatus m_status;
132+
// Written under m_playlistMutex, but read by unlocked getters from other
133+
// threads (status paths, the scheduler) -- atomic so those reads are
134+
// well-defined; volatile never made them so.
135+
std::atomic<PlaylistStatus> m_status;
133136

134137
Playlist* m_parent;
135138
std::string m_filename;

0 commit comments

Comments
 (0)