security: add total sample cache size limit in PulseAudio protocol

There was no limit on the total size of the sample cache. A client
could upload many samples to exhaust server memory. Add a configurable
pulse.max-sample-cache property (default 64MB) to cap the total size
of all cached samples.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Wim Taymans 2026-04-29 16:39:57 +02:00
parent 37990b5e90
commit 52afec565b
5 changed files with 18 additions and 0 deletions

View file

@ -120,6 +120,7 @@ pulse.properties = {
#pulse.min.quantum = 256/48000 # 5.3ms #pulse.min.quantum = 256/48000 # 5.3ms
#pulse.idle.timeout = 0 # don't pause after underruns #pulse.idle.timeout = 0 # don't pause after underruns
#pulse.max-streams = 64 # max streams per client #pulse.max-streams = 64 # max streams per client
#pulse.max-sample-cache = 67108864 # max total sample cache size (64MB)
#pulse.default.format = F32 #pulse.default.format = F32
#pulse.default.position = [ FL FR ] #pulse.default.position = [ FL FR ]
} }

View file

@ -77,6 +77,7 @@
* #pulse.default.position = [ FL FR ] * #pulse.default.position = [ FL FR ]
* #pulse.idle.timeout = 0 * #pulse.idle.timeout = 0
* #pulse.max-streams = 64 * #pulse.max-streams = 64
* #pulse.max-sample-cache = 67108864
* } * }
* *
* pulse.properties.rules = [ * pulse.properties.rules = [
@ -254,6 +255,13 @@
* *
* The maximum number of streams a single client can create. Default is 64. * The maximum number of streams a single client can create. Default is 64.
* *
*\code{.unparsed}
* pulse.max-sample-cache = 67108864
*\endcode
*
* The maximum total size in bytes of all sample cache entries. Default is
* 67108864 (64MB).
*
* ## Command execution * ## Command execution
* *
* As part of the server startup sequence, a set of commands can be executed. * As part of the server startup sequence, a set of commands can be executed.

View file

@ -38,6 +38,7 @@
#define MAX_NAME 1024u #define MAX_NAME 1024u
#define SCACHE_ENTRY_SIZE_MAX (1024*1024*16) #define SCACHE_ENTRY_SIZE_MAX (1024*1024*16)
#define MAX_SAMPLE_CACHE (1024u*1024*64) /* 64MB */
#define MAX_CLIENTS 64u #define MAX_CLIENTS 64u
#define MAX_STREAMS 64u #define MAX_STREAMS 64u

View file

@ -37,6 +37,7 @@ struct defs {
uint32_t quantum_limit; uint32_t quantum_limit;
uint32_t idle_timeout; uint32_t idle_timeout;
uint32_t max_streams; uint32_t max_streams;
uint32_t max_sample_cache;
}; };
struct stats { struct stats {

View file

@ -2413,6 +2413,12 @@ static int do_finish_upload_stream(struct client *client, uint32_t command, uint
channel, name); channel, name);
struct sample *old = find_sample(impl, SPA_ID_INVALID, name); struct sample *old = find_sample(impl, SPA_ID_INVALID, name);
uint32_t new_length = stream->attr.maxlength;
uint32_t old_length = old != NULL ? old->length : 0;
if (impl->stat.sample_cache + new_length - old_length > impl->defs.max_sample_cache) {
res = -ENOSPC;
goto error;
}
if (old == NULL || old->ref > 1) { if (old == NULL || old->ref > 1) {
sample = calloc(1, sizeof(*sample)); sample = calloc(1, sizeof(*sample));
if (sample == NULL) if (sample == NULL)
@ -5606,6 +5612,7 @@ static void load_defaults(struct defs *def, struct pw_properties *props)
parse_position(props, "pulse.default.position", DEFAULT_POSITION, &def->channel_map); parse_position(props, "pulse.default.position", DEFAULT_POSITION, &def->channel_map);
parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout); parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout);
parse_uint32(props, "pulse.max-streams", SPA_STRINGIFY(MAX_STREAMS), &def->max_streams); parse_uint32(props, "pulse.max-streams", SPA_STRINGIFY(MAX_STREAMS), &def->max_streams);
parse_uint32(props, "pulse.max-sample-cache", SPA_STRINGIFY(MAX_SAMPLE_CACHE), &def->max_sample_cache);
def->sample_spec.channels = def->channel_map.channels; def->sample_spec.channels = def->channel_map.channels;
def->quantum_limit = 8192; def->quantum_limit = 8192;
} }