Приходят 100 КБ webm с VP8-кадром гигантского разрешения (boundary у них буквально ----bomb*). Проверку f.Size > 5MB он проходит, validateWebm смотрит только первые 512 байт, а ffmpeg разворачивает такой кадр в ~1 ГБ сырых пикселей. При семафоре на 75 параллельных это укладывает box, чекер не укладывается в свои 7 секунд => MUMBLE. - video/probe.go: ffprobe читает geometry из заголовка (кадр не декодируется), режем > 4096 по стороне и > 8 Mpx. - Blur: image.DecodeConfig до image.Decode, тот же лимит по пикселям. imaging.Blur держит несколько копий битмапа, без лимита это OOM сам по себе. Заодно f.Close(), которого не было. - ffmpeg -threads 1, семафор 75 -> 16. - BodyLimit 8M: размер видео проверялся, а форма нет. Огромные субтитры превращались в слайс на миллионы строк в GenerateVtt. Плюс явный MaxSubtitlesSize 64K. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
1.8 KiB
Go
85 lines
1.8 KiB
Go
package video
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/disintegration/imaging"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ProcessingDeadline = 2 * time.Second
|
|
// 75 concurrent ffmpeg processes are enough to swap the box out on their
|
|
// own, bomb or no bomb.
|
|
semaphore = make(chan struct{}, 16)
|
|
)
|
|
|
|
func generatePreview(ctx context.Context, inp string, out string) error {
|
|
// Reject decompression bombs before ffmpeg expands a frame into memory.
|
|
if err := checkGeometry(ctx, inp); err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-threads", "1", "-i", inp, "-vframes", "1", out)
|
|
s := strings.Builder{}
|
|
cmd.Stdout = &s
|
|
cmd.Stderr = &s
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
fmt.Println(s.String())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func GeneratePreview(ctx context.Context, inp string, out string) error {
|
|
select {
|
|
case semaphore <- struct{}{}:
|
|
ctx, cancel := context.WithTimeout(ctx, ProcessingDeadline)
|
|
defer func() {
|
|
cancel()
|
|
<-semaphore
|
|
}()
|
|
return generatePreview(ctx, inp, out)
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func Blur(inp string, out string) error {
|
|
f, err := os.Open(inp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
// Same guard on the decode side: imaging.Blur keeps several copies of the
|
|
// bitmap alive, so an oversized preview is an OOM all by itself.
|
|
cfg, _, err := image.DecodeConfig(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cfg.Width*cfg.Height > MaxPixels {
|
|
return fmt.Errorf("preview %dx%d is too large to blur", cfg.Width, cfg.Height)
|
|
}
|
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
|
|
img, _, err := image.Decode(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outimg := imaging.Blur(img, 5)
|
|
outfile, err := os.Create(out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outfile.Close()
|
|
return png.Encode(outfile, outimg)
|
|
}
|