From 9edbcc2107ebbc091d2807e9ae1837f3d2451734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20=C3=98verli?= Date: Thu, 20 Aug 2026 19:47:01 +0200 Subject: [PATCH] fix(ytmusic): reject stale cookie cache writes --- external/ytmusic/cookie_provider.go | 13 +++++- external/ytmusic/cookie_provider_test.go | 54 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/external/ytmusic/cookie_provider.go b/external/ytmusic/cookie_provider.go index b164fc8..d18c0b9 100644 --- a/external/ytmusic/cookie_provider.go +++ b/external/ytmusic/cookie_provider.go @@ -36,6 +36,7 @@ type cookieBase struct { mu sync.Mutex playlists []playlist.PlaylistInfo trackCache map[string][]playlist.Track + generation uint64 nextLoad uint64 loadCancels map[uint64]context.CancelFunc } @@ -61,6 +62,7 @@ func (b *cookieBase) fetchPlaylists() ([]playlist.PlaylistInfo, error) { b.mu.Unlock() return res, nil } + generation := b.generation b.mu.Unlock() @@ -77,7 +79,9 @@ func (b *cookieBase) fetchPlaylists() ([]playlist.PlaylistInfo, error) { } b.mu.Lock() - b.playlists = pls + if b.generation == generation { + b.playlists = pls + } b.mu.Unlock() return pls, nil } @@ -88,6 +92,7 @@ func (b *cookieBase) fetchTracks(target string) ([]playlist.Track, error) { b.mu.Unlock() return cached, nil } + generation := b.generation ctx, cancel := context.WithTimeout(context.Background(), cookiePlaylistLoadTimeout) b.nextLoad++ @@ -122,13 +127,16 @@ func (b *cookieBase) fetchTracks(target string) ([]playlist.Track, error) { } b.mu.Lock() - b.trackCache[target] = tracks + if b.generation == generation { + b.trackCache[target] = tracks + } b.mu.Unlock() return tracks, nil } func (b *cookieBase) refresh() { b.mu.Lock() + b.generation++ for _, cancel := range b.loadCancels { cancel() } @@ -140,6 +148,7 @@ func (b *cookieBase) refresh() { func (b *cookieBase) close() { b.mu.Lock() + b.generation++ for _, cancel := range b.loadCancels { cancel() } diff --git a/external/ytmusic/cookie_provider_test.go b/external/ytmusic/cookie_provider_test.go index 2043285..12f94e3 100644 --- a/external/ytmusic/cookie_provider_test.go +++ b/external/ytmusic/cookie_provider_test.go @@ -335,6 +335,60 @@ func TestCookieProviderStopsTrackLoad(t *testing.T) { } } +func TestCookieProviderRefreshRejectsStaleResults(t *testing.T) { + t.Run("playlists", func(t *testing.T) { + base := newCookieBase("firefox") + started := make(chan struct{}) + release := make(chan struct{}) + base.fetchFn = func(string) ([]playlist.PlaylistInfo, error) { + close(started) + <-release + return []playlist.PlaylistInfo{{ID: "private"}}, nil + } + done := make(chan struct{}) + go func() { + _, _ = base.fetchPlaylists() + close(done) + }() + + <-started + base.refresh() + close(release) + <-done + base.mu.Lock() + defer base.mu.Unlock() + if base.playlists != nil { + t.Fatalf("stale playlists cached after refresh: %v", base.playlists) + } + }) + + t.Run("tracks", func(t *testing.T) { + base := newCookieBase("firefox") + started := make(chan struct{}) + release := make(chan struct{}) + base.resolveFn = func(context.Context, string, int, int, ...string) ([]playlist.Track, int, error) { + close(started) + <-release + return []playlist.Track{{Title: "Private"}}, 1, nil + } + done := make(chan struct{}) + go func() { + _, _ = base.fetchTracks("private") + close(done) + }() + + <-started + base.refresh() + close(release) + <-done + base.mu.Lock() + defer base.mu.Unlock() + if _, ok := base.trackCache["private"]; ok { + t.Fatal("stale tracks cached after refresh") + } + }) +} + func TestCookieProviderSearchTracksHonorsCancellation(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping Unix shell script test on Windows")