Commit 58b796baf9

58b796baf93d779ac6dee04ffd5aac189d4c9e02

parent: 9623fdfd99

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 05:41 UTC

texmath: refuse an argument that renders nothing; fuzz Convert

Ref #294

Layout: unified · split

internal/texmath/testdata/fuzz/FuzzConvert/1c174b99741b4473 added +3
@@ -0,0 +1,3 @@
1go test fuzz v1
2string("\\operatorname{\x00}")
3bool(false)
internal/texmath/texmath.go +11 −1
@@ -65,6 +65,11 @@ func Convert(tex string, display bool) (string, error) {
65 if !utf8.ValidString(tex) { 65 if !utf8.ValidString(tex) {
66 return "", errors.New("texmath: invalid UTF-8") 66 return "", errors.New("texmath: invalid UTF-8")
67 } 67 }
68 for _, r := range tex {
69 if r < 0x20 && r != '\t' && r != '\n' && r != '\r' || r == 0x7F || r == 0xFFFE || r == 0xFFFF {
70 return "", errors.New("texmath: control character")
71 }
72 }
68 p := &parser{src: tex, display: display} 73 p := &parser{src: tex, display: display}
69 kids, err := p.list(func(t token) bool { return false }) 74 kids, err := p.list(func(t token) bool { return false })
70 if err != nil { 75 if err != nil {
@@ -346,7 +351,12 @@ func (p *parser) arg() (*node, error) {
346 p.next() 351 p.next()
347 return leaf("mn", p.styled(t.val)), nil 352 return leaf("mn", p.styled(t.val)), nil
348 } 353 }
349 return p.atom() 354 n, err := p.atom()
355 if err == nil && n == nil {
356 // \displaystyle and the like render nothing, so cannot be an argument.
357 return nil, fmt.Errorf(`texmath: \%s cannot be an argument at %d`, t.val, t.pos)
358 }
359 return n, err
350} 360}
351 361
352func (p *parser) enter() error { 362func (p *parser) enter() error {
internal/texmath/texmath_test.go +55
@@ -1,6 +1,9 @@
1package texmath 1package texmath
2 2
3import ( 3import (
4 "encoding/xml"
5 "errors"
6 "io"
4 "regexp" 7 "regexp"
5 "strings" 8 "strings"
6 "testing" 9 "testing"
@@ -78,6 +81,8 @@ func TestConvertRefuses(t *testing.T) {
78 `\style{color:red}{x}`, `\color{red}{x}`, `\class{a}{x}`, `\htmlId{a}{x}`, 81 `\style{color:red}{x}`, `\color{red}{x}`, `\class{a}{x}`, `\htmlId{a}{x}`,
79 `\def\a{x}\a`, `\newcommand{\a}{x}`, `\require{html}`, `\unicode{x}`, 82 `\def\a{x}\a`, `\newcommand{\a}{x}`, `\require{html}`, `\unicode{x}`,
80 `\includegraphics{x}`, `\input{/etc/passwd}`, 83 `\includegraphics{x}`, `\input{/etc/passwd}`,
84 `\sqrt\displaystyle`, `\frac\displaystyle y`, `\hat\displaystyle`,
85 `\overbrace\displaystyle`, `\binom\displaystyle1`, `\mathbf\limits`, `x^\nolimits`,
81 } { 86 } {
82 if out, err := Convert(tex, false); err == nil { 87 if out, err := Convert(tex, false); err == nil {
83 t.Errorf("Convert(%q) = %s, want an error", tex, out) 88 t.Errorf("Convert(%q) = %s, want an error", tex, out)
@@ -167,3 +172,53 @@ func TestConvertEmitsOnlyListed(t *testing.T) {
167 } 172 }
168 } 173 }
169} 174}
175
176// FuzzConvert: no panic, output bounded by the input, and output that is
177// well-formed XML using only the listed elements and attribute values.
178func FuzzConvert(f *testing.F) {
179 for _, seed := range []string{
180 `x^2`, `\frac{a}{b}`, `\sqrt[3]{x}`, `\left(\frac12\right)`, `\sum_{i=1}^n i`,
181 `\begin{pmatrix}a&b\\c&d\end{pmatrix}`, `\text{a<b}`, `\mathbb{R}`, `\hat{x}~'`,
182 `\sqrt\displaystyle`, `\operatorname*{argmax}_x`, `{{{x}}}`, `a\,b\quad c`,
183 } {
184 f.Add(seed, false)
185 }
186 allowed := map[string]bool{}
187 for _, e := range Elements {
188 allowed[e] = true
189 }
190 length := regexp.MustCompile(`^-?[0-9]+(\.[0-9]+)?em$`)
191 f.Fuzz(func(t *testing.T, tex string, display bool) {
192 out, err := Convert(tex, display)
193 if err != nil {
194 return
195 }
196 if len(out) > 64*len(tex)+256 {
197 t.Fatalf("%d bytes out for %d in", len(out), len(tex))
198 }
199 d := xml.NewDecoder(strings.NewReader(out))
200 for {
201 tok, err := d.Token()
202 if errors.Is(err, io.EOF) {
203 break
204 }
205 if err != nil {
206 t.Fatalf("not XML: %v\n%s", err, out)
207 }
208 start, ok := tok.(xml.StartElement)
209 if !ok {
210 continue
211 }
212 if !allowed[start.Name.Local] || start.Name.Space != "" {
213 t.Fatalf("element %v in %s", start.Name, out)
214 }
215 for _, a := range start.Attr {
216 want, ok := Attrs[start.Name.Local][a.Name.Local]
217 if !ok || a.Name.Space != "" || (want == "<length>" && !length.MatchString(a.Value)) ||
218 (want != "<length>" && want != a.Value) {
219 t.Fatalf("attribute %v=%q on %s in %s", a.Name, a.Value, start.Name.Local, out)
220 }
221 }
222 }
223 })
224}