fix(lint): trailing-label CFG guard and the goroutine alias

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-19 23:49:19 +02:00
parent a8bfd54ed2
commit eb8b0cd316
4 changed files with 176 additions and 15 deletions
+36 -4
View File
@@ -4,7 +4,7 @@
package lint
import (
"sort"
"slices"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
@@ -86,6 +86,15 @@ func (l *liveness) buildCFG(t *ast.Text) {
labelToBlock[b.label] = i
}
}
// A trailing label whose block was never flushed (no instruction follows
// it) still holds the index the next block would have taken, which is one
// past the end. Drop those entries so a branch to such a label wires no
// edge instead of indexing past the live sets in the dataflow.
for name, v := range labelToBlock {
if v >= len(l.blocks) {
delete(labelToBlock, name)
}
}
for i, b := range l.blocks {
if len(b.instrs) == 0 {
if i+1 < len(l.blocks) {
@@ -210,9 +219,13 @@ func isUnconditionalBranchAny(m string) bool {
return false
}
// branchTarget returns the local-label target of a branch, if it is one.
// branchTarget returns the local-label target of a branch, if it is one. The
// last bare-symbol operand is taken because the Plan 9 branch target sits in
// the destination position, after any operand registers: CBZ R0, done or
// BEQ R1, R2, done. Scanning forward would pick R0 up as the target.
func branchTarget(in *ast.Instr) (string, bool) {
for _, op := range in.Operands {
for _, op := range slices.Backward(in.Operands) {
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
op.Addr.Base == "" && op.Addr.Sym.Name != "" {
return op.Addr.Sym.Name, true
@@ -294,6 +307,20 @@ func isCompare(m string) bool {
m == "FCMP" || m == "FCMPE"
}
// goroutineAlias maps the assembler's architectural alias for the goroutine
// register to its numeric name, verified against go tool asm of the Go 1.27
// toolchain: `MOVQ AX, g` encodes the same bytes as R14 on amd64, and on
// arm64, riscv64 and loong64 the alias is the ONLY spelling the toolchain
// accepts (a numeric R28, X27 or R22 operand is rejected), so a kernel can
// clobber the goroutine register through `g` alone. The name is
// case-sensitive: only lowercase `g` assembles.
var goroutineAlias = map[arch.Arch]string{
arch.AMD64: "R14",
arch.ARM64: "R28",
arch.RISCV: "X27",
arch.LOONG64: "R22",
}
// gprName returns the canonical general-purpose register name of an operand, or
// "" if the operand is not a bare GPR reference.
func gprName(op *ast.Operand, a arch.Arch) string {
@@ -307,6 +334,11 @@ func gprName(op *ast.Operand, a arch.Arch) string {
if r, ok := arch.ForArch(a).Register(name); ok && (r.Class == arch.GPR || r.Class == arch.GPRSub) {
return canonicalGPR(name)
}
// The goroutine alias is not an arch-table register; resolve it so a
// clobber written through `g` is audited like the numeric register.
if n, ok := goroutineAlias[a]; ok && name == "g" {
return n
}
return ""
}
@@ -445,7 +477,7 @@ func clobberedGoFixed(l *liveness, a arch.Arch, reachesRuntime bool) (always, ru
out = append(out, r)
}
}
sort.Strings(out)
slices.Sort(out)
return out
}
always = clobbered(alwaysSet)