From 9dba6d85e8a6a67b5f7f1402e5433a26ef9f7366 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Thu, 20 Aug 2026 17:22:06 +0200 Subject: [PATCH 1/3] command/StickerCommands: fix crash in "sticker delete" without a name The three-argument form of "sticker delete" deletes all stickers of a URI and passes a null "name" to DomainHandler::Delete(). If the delete affects no rows, the error path formats that null pointer with libfmt, which aborts the process, so any client can terminate the daemon with sticker delete song "foo.ogg" on a song that has no stickers. Report the URI instead when no name was given. --- src/command/StickerCommands.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/command/StickerCommands.cxx b/src/command/StickerCommands.cxx index 82080f9bfd..0061da15bb 100644 --- a/src/command/StickerCommands.cxx +++ b/src/command/StickerCommands.cxx @@ -84,7 +84,12 @@ class DomainHandler { ? sticker_database.Delete(sticker_type, uri) : sticker_database.DeleteValue(sticker_type, uri, name); if (!ret) { - response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: {:?}", name); + if (name == nullptr) + response.FmtError(ACK_ERROR_NO_EXIST, + "no stickers found: {:?}", uri); + else + response.FmtError(ACK_ERROR_NO_EXIST, + "no such sticker: {:?}", name); return CommandResult::ERROR; } From be069a6281175c69e6951a6bb3666807e0483fdb Mon Sep 17 00:00:00 2001 From: Jon Ferguson Date: Thu, 20 Aug 2026 15:34:57 -0400 Subject: [PATCH 2/3] output/Source: cross-fade using the ReplayGainFilter's output format When software volume is enabled (mixer_type "software"), the ReplayGainFilter converts S16 input to S24_P32 (PcmVolume::Open() with allow_convert=true) to avoid losing precision. FilterChunk() applied that filter to both chunks and then called pcm_mix() with in_audio_format.format (S16) on data that was already S24_P32. Mixing 32-bit samples as if they were 16-bit produces loud noise for the whole cross-fade duration. OpenFilter() already uses the ReplayGainFilter's output format for the rest of the filter chain; use it for the cross-fade mix as well. Fixes #2050, probably also #1101. --- src/output/Source.cxx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/output/Source.cxx b/src/output/Source.cxx index 7c3d557f17..d46a779bbb 100644 --- a/src/output/Source.cxx +++ b/src/output/Source.cxx @@ -185,13 +185,22 @@ AudioOutputSource::FilterChunk(const MusicChunk &chunk) case */ mix_ratio = 1.0f - mix_ratio; + /* use the post-replay-gain format for mixing; + the ReplayGainFilter may convert the sample + format (e.g. S16 to S24_P32 when software + volume is enabled) */ + const SampleFormat cross_fade_format = + replay_gain_filter + ? replay_gain_filter->GetOutAudioFormat().format + : in_audio_format.format; + void *dest = cross_fade_buffer.Get(other_data.size()); memcpy(dest, other_data.data(), other_data.size()); if (!pcm_mix(cross_fade_dither, dest, data.data(), data.size(), - in_audio_format.format, + cross_fade_format, mix_ratio)) throw FmtRuntimeError("Cannot cross-fade format {}", - in_audio_format.format); + cross_fade_format); data = {(const std::byte *)dest, other_data.size()}; } From daf13d34936e6ec494ef0e90965874f8de2c3676 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Fri, 21 Aug 2026 11:28:33 -0400 Subject: [PATCH 3/3] playlist/pls: limit input size The PLS parser materializes up to 65,536 entries before returning any songs. Each entry can retain a file URI and title near the text line limit, allowing a playlist to consume hundreds of MiB. Add an InputStream proxy which enforces a byte limit and wrap PLS input with a 16 MiB budget. Known-size streams are rejected before parsing, while the read limit also covers streams whose size is not known in advance. --- src/input/LimitedInputStream.cxx | 52 ++++++++++++++++++++++ src/input/LimitedInputStream.hxx | 25 +++++++++++ src/input/meson.build | 1 + src/playlist/plugins/PlsPlaylistPlugin.cxx | 8 +++- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/input/LimitedInputStream.cxx create mode 100644 src/input/LimitedInputStream.hxx diff --git a/src/input/LimitedInputStream.cxx b/src/input/LimitedInputStream.cxx new file mode 100644 index 0000000000..617789fa4b --- /dev/null +++ b/src/input/LimitedInputStream.cxx @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "LimitedInputStream.hxx" + +#include +#include + +LimitedInputStream::LimitedInputStream(InputStreamPtr _input, + const offset_type max_size) + :ProxyInputStream(std::move(_input)), remaining(max_size) +{ + ProxyInputStream::Update(); + Check(); +} + +void +LimitedInputStream::CheckSize() +{ + if (size_checked || !IsReady()) + return; + + if (input->KnownSize() && input->GetRest() > remaining) + throw std::runtime_error("Input stream is too large"); + + size_checked = true; +} + +void +LimitedInputStream::Check() +{ + ProxyInputStream::Check(); + CheckSize(); +} + +std::size_t +LimitedInputStream::Read(std::unique_lock &lock, + std::span dest) +{ + assert(!dest.empty()); + + if (remaining < dest.size()) + dest = dest.first(static_cast(remaining) + 1); + + const std::size_t nbytes = input->Read(lock, dest); + if (nbytes > remaining) + throw std::runtime_error("Input stream is too large"); + + remaining -= nbytes; + CopyAttributes(); + return nbytes; +} diff --git a/src/input/LimitedInputStream.hxx b/src/input/LimitedInputStream.hxx new file mode 100644 index 0000000000..b71300ce60 --- /dev/null +++ b/src/input/LimitedInputStream.hxx @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include "ProxyInputStream.hxx" + +/** + * An #InputStream proxy which limits the amount of data that may be + * read from the underlying stream. + */ +class LimitedInputStream final : public ProxyInputStream { + offset_type remaining; + bool size_checked = false; + + void CheckSize(); + +public: + LimitedInputStream(InputStreamPtr _input, offset_type max_size); + + /* virtual methods from class InputStream */ + void Check() override; + std::size_t Read(std::unique_lock &lock, + std::span dest) override; +}; diff --git a/src/input/meson.build b/src/input/meson.build index b1f6b78b8f..21fcf2108d 100644 --- a/src/input/meson.build +++ b/src/input/meson.build @@ -23,6 +23,7 @@ input_basic = static_library( 'input_basic', 'AsyncInputStream.cxx', 'LastInputStream.cxx', + 'LimitedInputStream.cxx', 'MemoryInputStream.cxx', 'ProxyInputStream.cxx', 'RewindInputStream.cxx', diff --git a/src/playlist/plugins/PlsPlaylistPlugin.cxx b/src/playlist/plugins/PlsPlaylistPlugin.cxx index 81c05913fa..fa4b9a318c 100644 --- a/src/playlist/plugins/PlsPlaylistPlugin.cxx +++ b/src/playlist/plugins/PlsPlaylistPlugin.cxx @@ -4,6 +4,7 @@ #include "PlsPlaylistPlugin.hxx" #include "../PlaylistPlugin.hxx" #include "../MemorySongEnumerator.hxx" +#include "input/LimitedInputStream.hxx" #include "input/TextInputStream.hxx" #include "input/InputStream.hxx" #include "song/DetachedSong.hxx" @@ -19,6 +20,9 @@ using std::string_view_literals::operator""sv; +/* PLS playlists are materialized before any songs are returned. */ +static constexpr offset_type PLS_PLAYLIST_MAX_SIZE = 16 * 1024 * 1024; + static bool FindPlaylistSection(TextInputStream &is) { @@ -133,7 +137,9 @@ ParsePls(TextInputStream &is, std::forward_list &songs) static bool ParsePls(InputStreamPtr &&is, std::forward_list &songs) { - TextInputStream tis(std::move(is)); + InputStreamPtr limited = std::make_unique + (std::move(is), PLS_PLAYLIST_MAX_SIZE); + TextInputStream tis(std::move(limited)); if (!ParsePls(tis, songs)) { is = tis.StealInputStream(); return false;