bound the configured audio buffer count

Buffers had a lower bound but no upper one, and it is allocated per speaking
user twice over: as a queue of decoded frames and as OpenAL playback
buffers. Each buffer holds up to one maximum sized frame, so a large value
multiplied by a populated channel is a substantial amount of memory.

Cap it, and reject the flag up front so the failure names the flag rather
than surfacing later as a dial error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-24 12:32:44 -04:00
co-authored by Claude Opus 5
parent 0d62f745ca
commit ef9bd19fc9
3 changed files with 33 additions and 0 deletions
+12
View File
@@ -38,6 +38,12 @@ type Config struct {
Buffers int Buffers int
} }
// MaximumBuffers caps Config.Buffers. Each buffer holds up to one maximum
// sized audio frame and is allocated per speaking user, so a large value
// multiplied by a populated channel is a substantial amount of memory. A few
// seconds of buffering is already far more than playback needs.
const MaximumBuffers = 1024
// NewConfig returns a new Config struct with default values set. // NewConfig returns a new Config struct with default values set.
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
@@ -64,6 +70,12 @@ func (c *Config) Validate() error {
if c.Buffers <= 0 { if c.Buffers <= 0 {
return fmt.Errorf("gumble: Buffers must be positive") return fmt.Errorf("gumble: Buffers must be positive")
} }
// Buffers is allocated per speaking user, both as a queue of decoded
// frames and as OpenAL playback buffers, so an unbounded value multiplies
// straight into memory use as a channel fills up.
if c.Buffers > MaximumBuffers {
return fmt.Errorf("gumble: Buffers must be at most %d", MaximumBuffers)
}
return nil return nil
} }
@@ -0,0 +1,17 @@
package gumble
import "testing"
// Regression: Buffers had no upper bound, but it is allocated per speaking
// user both as a decoded-frame queue and as OpenAL playback buffers.
func TestConfigRejectsOversizedBuffers(t *testing.T) {
config := NewConfig()
config.Buffers = MaximumBuffers + 1
if err := config.Validate(); err == nil {
t.Fatal("expected Buffers above the maximum to be rejected")
}
config.Buffers = MaximumBuffers
if err := config.Validate(); err != nil {
t.Fatalf("Buffers at the maximum should be accepted: %v", err)
}
}
+4
View File
@@ -105,6 +105,10 @@ func main() {
if err != nil { if err != nil {
handle_raw_error(err) handle_raw_error(err)
} }
if *buffers <= 0 || *buffers > gumble.MaximumBuffers {
handle_raw_error(fmt.Errorf("buffers must be between 1 and %d, got %d",
gumble.MaximumBuffers, *buffers))
}
// Set up logging // Set up logging
var level barnlog.Level var level barnlog.Level