detached f9ca745

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +39 -2
M spa/pod.go +5 -2
@@ -3,6 +3,7 @@ package spa
33 import (
44 "encoding/binary"
55 "errors"
6+ "math"
67 )
78
89 // Errors returned by the pod builder.
@@ -149,7 +150,8 @@ func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num
149150
150151 // choiceBegin writes the choice header; it returns the header offset.
151152 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) {
153155 return 0, false
154156 }
155157 start := b.n
@@ -271,7 +273,8 @@ func (b *PodBuilder) podEnumFormats(formats []VideoFormat) {
271273 // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and
272274 // the allowed SPA data types as a bit mask (for example 1<<DataMemFd).
273275 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 {
275278 return nil, ErrPodState
276279 }
277280 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, false155 return 0, false
154 }156 }
155 start := b.n157 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) and273 // 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, ErrPodState278 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
33 import (
44 "encoding/binary"
55 "encoding/hex"
6+ "errors"
67 "testing"
78 )
89
@@ -58,6 +59,39 @@ func TestVideoBuffersGolden(t *testing.T) {
5859 }
5960 }
6061
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+
6195 func TestMetaHeaderGolden(t *testing.T) {
6296 var dst [128]byte
6397 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]byte96 var dst [128]byte
63 got, err := MetaHeader(dst[:])97 got, err := MetaHeader(dst[:])

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close