From bbe14dd1ab45fccd29ece694003729eda500780d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 12:56:25 +0200 Subject: [PATCH] fix(parser): diagnose GLOBL and DATA without a symbol name A bare GLOBL or DATA parsed without a single diagnostic while leaving a nil Name in the tree, a pointer every downstream tool dereferences; TEXT keeps a placeholder beside its error for exactly that reason, and GLOBL and DATA now do the same. The crashing input enters the corpus. Assisted-by: GLM 5.3 --- .../c83eeb7a26883de3 | 2 ++ parser/parser.go | 13 +++++++ parser/parser_test.go | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 format/testdata/fuzz/FuzzFormatExpansionRoundTrip/c83eeb7a26883de3 diff --git a/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/c83eeb7a26883de3 b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/c83eeb7a26883de3 new file mode 100644 index 0000000..c1b6a32 --- /dev/null +++ b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/c83eeb7a26883de3 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("DATA") diff --git a/parser/parser.go b/parser/parser.go index c49e629..a89f5ea 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -299,6 +299,13 @@ func (p *state) parseGlobl(line []token.Token) *ast.Globl { g := &ast.Globl{Keyword: line[0]} rest := skipComma(line[1:]) sym, n := parseSymbolPrefix(rest) + if sym == nil { + // A usable tree without a name cannot be had, and the linter and LSP + // dereference Name on every parsed GLOBL, so the decl keeps a + // placeholder beside its error, exactly as TEXT does. + p.errorf(line[0].Pos, "GLOBL missing a symbol name") + sym = &ast.Symbol{Pos: line[0].Pos, Raw: "?", Name: "?"} + } g.Name = sym rest = skipComma(rest[n:]) // Flags are identifiers (RODATA, DUPOK) or legacy numeric constants @@ -324,6 +331,12 @@ func (p *state) parseData(line []token.Token) *ast.Data { nameGroup, valuePart := splitFirstComma(rest) nameGroup, width := splitTrailingWidth(nameGroup) sym, _ := parseSymbolPrefix(nameGroup) + if sym == nil { + // Same contract as TEXT and GLOBL: the tree stays usable beside its + // error, because downstream tools dereference Name unconditionally. + p.errorf(line[0].Pos, "DATA missing a symbol name") + sym = &ast.Symbol{Pos: line[0].Pos, Raw: "?", Name: "?"} + } d.Name = sym d.Width = width if len(valuePart) > 0 { diff --git a/parser/parser_test.go b/parser/parser_test.go index c75ab0c..25bfe87 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -383,6 +383,40 @@ func TestTextMissingSymbolKeepsDecl(t *testing.T) { } } +// TestGloblDataMissingSymbolKeepsDecl holds GLOBL and DATA to TEXT's +// contract: a directive with no symbol name is a diagnostic, and the decl +// stays in the tree with a non-nil placeholder, because downstream tools +// dereference Name unconditionally. A bare "DATA" used to parse without a +// single error while leaving Name nil behind, a landmine every consumer +// stepped on. +func TestGloblDataMissingSymbolKeepsDecl(t *testing.T) { + for _, tc := range []struct { + src string + kind string + }{ + {"GLOBL , $8\n", "GLOBL"}, + {"DATA\n", "DATA"}, + {"DATA /4, $1\n", "DATA"}, + } { + file, errs := Parse("t.s", tc.src) + if len(errs) == 0 { + t.Errorf("%s with no symbol: want a diagnostic", tc.kind) + } + var name *ast.Symbol + switch d := file.Decls[0].(type) { + case *ast.Globl: + name = d.Name + case *ast.Data: + name = d.Name + default: + t.Fatalf("%s: decl is %T", tc.kind, d) + } + if name == nil { + t.Errorf("%s: Name must never be nil: downstream tools dereference it", tc.kind) + } + } +} + // TestInt64MinimumImmediate covers $-0x8000000000000000: the digits overflow // int64 when parsed directly, but the negated magnitude is exactly the int64 // minimum and must land in Val rather than the float fallback.