fix(spa): size choice room checks to their padding and bound buffer sizes to int32
choiceBegin reserved eight bytes beyond the padded choice, so a destination exactly as long as the pod failed with ErrPodFull. VideoBuffers rejects sizes and strides that do not fit the int32 the pod carries.
- commit
- f9ca745a678d3daf2ccfc70f7afcc3f5fcbbaaba
- author
- brice · · 2026-10-14 21:04 UTC
- parent
- fcbefd1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +39 -2
M spa/pod.go +5 -2
| @@ -3,6 +3,7 @@ package spa | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "errors" | |
| 6 | + | "math" | |
| 6 | 7 | ) | |
| 7 | 8 | ||
| 8 | 9 | // Errors returned by the pod builder. | |
| @@ -149,7 +150,8 @@ func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num | |||
| 149 | 150 | ||
| 150 | 151 | // choiceBegin writes the choice header; it returns the header offset. | |
| 151 | 152 | func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) { | |
| 152 | - | if !b.room(24 + nvals*int(childSize) + 8) { | |
| 153 | + | // Room for the header, the values and the padding choiceEnd adds. | |
| 154 | + | if !b.room((b.n+24+nvals*int(childSize)+7)&^7 - b.n) { | |
| 153 | 155 | return 0, false | |
| 154 | 156 | } | |
| 155 | 157 | start := b.n | |
| @@ -271,7 +273,8 @@ func (b *PodBuilder) podEnumFormats(formats []VideoFormat) { | |||
| 271 | 273 | // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and | |
| 272 | 274 | // the allowed SPA data types as a bit mask (for example 1<<DataMemFd). | |
| 273 | 275 | func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) { | |
| 274 | - | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf { | |
| 276 | + | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf || | |
| 277 | + | size > math.MaxInt32 || stride > math.MaxInt32 || maxBuf > math.MaxInt32 { | |
| 275 | 278 | return nil, ErrPodState | |
| 276 | 279 | } | |
| 277 | 280 | b := NewPodBuilder(dst) | |
| @@ -3,6 +3,7 @@ package spa | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | + | "math" | |||
| 6 | ) | 7 | ) | ||
| 7 | 8 | ||||
| 8 | // Errors returned by the pod builder. | 9 | // Errors returned by the pod builder. | ||
| @@ -149,7 +150,8 @@ func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num | |||||
| 149 | 150 | ||||
| 150 | // choiceBegin writes the choice header; it returns the header offset. | 151 | // choiceBegin writes the choice header; it returns the header offset. | ||
| 151 | func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) { | 152 | func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) { | ||
| 152 | - | if !b.room(24 + nvals*int(childSize) + 8) { | 153 | + | // Room for the header, the values and the padding choiceEnd adds. |
| 154 | + | if !b.room((b.n+24+nvals*int(childSize)+7)&^7 - b.n) { | |||
| 153 | return 0, false | 155 | return 0, false | ||
| 154 | } | 156 | } | ||
| 155 | start := b.n | 157 | start := b.n | ||
| @@ -271,7 +273,8 @@ func (b *PodBuilder) podEnumFormats(formats []VideoFormat) { | |||||
| 271 | // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and | 273 | // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and | ||
| 272 | // the allowed SPA data types as a bit mask (for example 1<<DataMemFd). | 274 | // the allowed SPA data types as a bit mask (for example 1<<DataMemFd). | ||
| 273 | func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) { | 275 | func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) { | ||
| 274 | - | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf { | 276 | + | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf || |
| 277 | + | size > math.MaxInt32 || stride > math.MaxInt32 || maxBuf > math.MaxInt32 { | |||
| 275 | return nil, ErrPodState | 278 | return nil, ErrPodState | ||
| 276 | } | 279 | } | ||
| 277 | b := NewPodBuilder(dst) | 280 | b := NewPodBuilder(dst) | ||
M spa/pod_test.go +34 -0
| @@ -3,6 +3,7 @@ package spa | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "encoding/hex" | |
| 6 | + | "errors" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| @@ -58,6 +59,39 @@ func TestVideoBuffersGolden(t *testing.T) { | |||
| 58 | 59 | } | |
| 59 | 60 | } | |
| 60 | 61 | ||
| 62 | + | // TestBuildersExactFit checks each builder accepts a destination exactly as | |
| 63 | + | // long as its pod and rejects one byte less. | |
| 64 | + | func TestBuildersExactFit(t *testing.T) { | |
| 65 | + | builds := map[string]func(dst []byte) ([]byte, error){ | |
| 66 | + | "enum single": func(dst []byte) ([]byte, error) { return VideoEnumFormat(dst, 320, 240, 60, VideoFormatBGRx) }, | |
| 67 | + | "enum multi": func(dst []byte) ([]byte, error) { | |
| 68 | + | return VideoEnumFormat(dst, 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA) | |
| 69 | + | }, | |
| 70 | + | "buffers": func(dst []byte) ([]byte, error) { return VideoBuffers(dst, 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) }, | |
| 71 | + | "meta": MetaHeader, | |
| 72 | + | } | |
| 73 | + | for name, build := range builds { | |
| 74 | + | var big [512]byte | |
| 75 | + | pod, err := build(big[:]) | |
| 76 | + | if err != nil { | |
| 77 | + | t.Fatalf("%s: %v", name, err) | |
| 78 | + | } | |
| 79 | + | if _, err := build(make([]byte, len(pod))); err != nil { | |
| 80 | + | t.Errorf("%s: exact %d-byte destination: %v", name, len(pod), err) | |
| 81 | + | } | |
| 82 | + | if _, err := build(make([]byte, len(pod)-1)); !errors.Is(err, ErrPodFull) { | |
| 83 | + | t.Errorf("%s: short destination: %v, want ErrPodFull", name, err) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func TestVideoBuffersRejectsInt32Overflow(t *testing.T) { | |
| 89 | + | var dst [512]byte | |
| 90 | + | if _, err := VideoBuffers(dst[:], 1<<31, 1<<17, 2, 8, 16, 1<<DataMemFd); !errors.Is(err, ErrPodState) { | |
| 91 | + | t.Fatalf("size 1<<31: %v, want ErrPodState", err) | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | ||
| 61 | 95 | func TestMetaHeaderGolden(t *testing.T) { | |
| 62 | 96 | var dst [128]byte | |
| 63 | 97 | got, err := MetaHeader(dst[:]) | |
| @@ -3,6 +3,7 @@ package spa | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | "encoding/hex" | 5 | "encoding/hex" | ||
| 6 | + | "errors" | |||
| 6 | "testing" | 7 | "testing" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| @@ -58,6 +59,39 @@ func TestVideoBuffersGolden(t *testing.T) { | |||||
| 58 | } | 59 | } | ||
| 59 | } | 60 | } | ||
| 60 | 61 | ||||
| 62 | + | // TestBuildersExactFit checks each builder accepts a destination exactly as | |||
| 63 | + | // long as its pod and rejects one byte less. | |||
| 64 | + | func TestBuildersExactFit(t *testing.T) { | |||
| 65 | + | builds := map[string]func(dst []byte) ([]byte, error){ | |||
| 66 | + | "enum single": func(dst []byte) ([]byte, error) { return VideoEnumFormat(dst, 320, 240, 60, VideoFormatBGRx) }, | |||
| 67 | + | "enum multi": func(dst []byte) ([]byte, error) { | |||
| 68 | + | return VideoEnumFormat(dst, 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA) | |||
| 69 | + | }, | |||
| 70 | + | "buffers": func(dst []byte) ([]byte, error) { return VideoBuffers(dst, 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) }, | |||
| 71 | + | "meta": MetaHeader, | |||
| 72 | + | } | |||
| 73 | + | for name, build := range builds { | |||
| 74 | + | var big [512]byte | |||
| 75 | + | pod, err := build(big[:]) | |||
| 76 | + | if err != nil { | |||
| 77 | + | t.Fatalf("%s: %v", name, err) | |||
| 78 | + | } | |||
| 79 | + | if _, err := build(make([]byte, len(pod))); err != nil { | |||
| 80 | + | t.Errorf("%s: exact %d-byte destination: %v", name, len(pod), err) | |||
| 81 | + | } | |||
| 82 | + | if _, err := build(make([]byte, len(pod)-1)); !errors.Is(err, ErrPodFull) { | |||
| 83 | + | t.Errorf("%s: short destination: %v, want ErrPodFull", name, err) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func TestVideoBuffersRejectsInt32Overflow(t *testing.T) { | |||
| 89 | + | var dst [512]byte | |||
| 90 | + | if _, err := VideoBuffers(dst[:], 1<<31, 1<<17, 2, 8, 16, 1<<DataMemFd); !errors.Is(err, ErrPodState) { | |||
| 91 | + | t.Fatalf("size 1<<31: %v, want ErrPodState", err) | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | ||||
| 61 | func TestMetaHeaderGolden(t *testing.T) { | 95 | func TestMetaHeaderGolden(t *testing.T) { | ||
| 62 | var dst [128]byte | 96 | var dst [128]byte | ||
| 63 | got, err := MetaHeader(dst[:]) | 97 | got, err := MetaHeader(dst[:]) | ||