fix(audio): stop reading ffprobe output past a cap and call the file unreadable (#49)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:56:47 +02:00
co-authored by Claude Sonnet 5.5
parent d44db88787
commit 089444c19f
+52 -4
View File
@@ -4,6 +4,7 @@
package audio package audio
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -19,6 +20,45 @@ import (
type FFprobe struct { type FFprobe struct {
// Bin is the ffprobe executable ("ffprobe" resolves via PATH). // Bin is the ffprobe executable ("ffprobe" resolves via PATH).
Bin string Bin string
// MaxOutput bounds how much of ffprobe's stdout is read; zero means DefaultMaxOutput.
MaxOutput int
}
// DefaultMaxOutput is far above the narrowed probe output of a legitimate file (a few
// hundred bytes) and far below what a hostile one can make ffprobe print.
const DefaultMaxOutput = 1 << 20
// maxStderr is how much of ffprobe's stderr is kept for the error message.
const maxStderr = 4096
// errOutputCap is the write error that stops ffprobe once it prints past the cap.
var errOutputCap = errors.New("audio: ffprobe output exceeds the cap")
// capWriter collects up to max bytes and then refuses further writes, which closes the
// pipe and ends the process instead of buffering whatever it prints.
type capWriter struct {
buf bytes.Buffer
max int
}
func (w *capWriter) Write(p []byte) (int, error) {
if w.buf.Len()+len(p) > w.max {
return 0, errOutputCap
}
return w.buf.Write(p)
}
// headWriter keeps the first max bytes and silently drops the rest.
type headWriter struct {
buf bytes.Buffer
max int
}
func (w *headWriter) Write(p []byte) (int, error) {
if room := w.max - w.buf.Len(); room > 0 {
w.buf.Write(p[:min(room, len(p))])
}
return len(p), nil
} }
// AllowedDemuxers are the input formats an upload may be. The input is // AllowedDemuxers are the input formats an upload may be. The input is
@@ -64,18 +104,26 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error)
args = append(args, "-print_format", "json", "-show_entries", probeEntries, path) args = append(args, "-print_format", "json", "-show_entries", probeEntries, path)
cmd := exec.CommandContext(ctx, bin, args...) cmd := exec.CommandContext(ctx, bin, args...)
cmd.WaitDelay = killGrace cmd.WaitDelay = killGrace
out, err := cmd.Output() maxOut := f.MaxOutput
if err != nil { if maxOut <= 0 {
maxOut = DefaultMaxOutput
}
stdout, stderr := &capWriter{max: maxOut}, &headWriter{max: maxStderr}
cmd.Stdout, cmd.Stderr = stdout, stderr
if err := cmd.Run(); err != nil {
if ctxErr := ctx.Err(); ctxErr != nil { if ctxErr := ctx.Err(); ctxErr != nil {
return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr) return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr)
} }
if errors.Is(err, errOutputCap) {
return uploads.Probe{}, fmt.Errorf("%w: ffprobe printed more than %d bytes", uploads.ErrUnreadable, maxOut)
}
var exit *exec.ExitError var exit *exec.ExitError
if errors.As(err, &exit) { if errors.As(err, &exit) {
return uploads.Probe{}, fmt.Errorf("%w: ffprobe: %s", uploads.ErrUnreadable, exit.Stderr) return uploads.Probe{}, fmt.Errorf("%w: ffprobe: %s", uploads.ErrUnreadable, stderr.buf.String())
} }
return uploads.Probe{}, fmt.Errorf("audio: run ffprobe: %w", err) return uploads.Probe{}, fmt.Errorf("audio: run ffprobe: %w", err)
} }
p, err := ParseProbe(out) p, err := ParseProbe(stdout.buf.Bytes())
if err != nil { if err != nil {
return uploads.Probe{}, fmt.Errorf("audio: ffprobe output: %w", err) return uploads.Probe{}, fmt.Errorf("audio: ffprobe output: %w", err)
} }