pulsar: GATE-C indirection lint + honest soundness wording (RED INFO)

The AST name call-graph follows DIRECT named calls only; it misses function-
value / closure / go:linkname indirection (RED PoC: e:=deriveKeyMaterial;
s:=bccSign; s(e(...)) not flagged). The old comment overstated 'never miss one'.

Fix: pair the reach graph with a companion lint forbidBannedPrimitiveIndirection
that forbids taking ANY banned primitive as a non-call value or aliasing it via
//go:linkname anywhere in the default build. With that lint green, every banned-
primitive use in the build IS a direct call the graph follows — so the PAIR is
complete for the banned set (neither half claimed complete alone). Wording
softened to state exactly this.

GATE: TestGATE_C_NoBannedPrimitiveValueIndirection — (a) the real build takes no
banned primitive as a value / linkname; (b) the name graph alone MISSES the
e:=deriveKeyMaterial; s:=bccSign indirection while the lint CATCHES it; (c)
go:linkname aliasing a banned primitive is caught.
This commit is contained in:
zeekay
2026-06-27 22:36:52 -07:00
parent 0bc2523566
commit 5f2722f987
+162 -6
View File
@@ -13,12 +13,24 @@ package pulsar
// default-build call graph from ANY committee sign entrypoint. It is a stdlib
// (go/ast) name call-graph — no new dependency. The callee side is
// OVER-APPROXIMATED by name (a CallExpr to name X follows every definition
// named X), which is the SOUND bias for a security gate: it can over-report a
// reach, never miss one.
// named X), which is the SOUND bias for a security gate over DIRECT CALLS.
//
// GATE C is two tests: (1) the REAL package is clean (the invariant holds);
// (2) the checker actually CATCHES a deriveKeyMaterial+bccSign bypass wired into
// a sign entrypoint (a synthetic graph modeling RED's PoC).
// SCOPE / SOUNDNESS — honest statement (RED INFO). A name call-graph follows
// DIRECT named calls only; on its own it would MISS function-VALUE / closure /
// `//go:linkname` indirection (e.g. `e := deriveKeyMaterial; s := bccSign;
// s(e(...))`). So the reachability graph does NOT stand alone — it is paired
// with a companion lint (forbidBannedPrimitiveValueUse + the go:linkname scan)
// that FORBIDS taking any banned primitive as a non-call value or aliasing it
// via go:linkname anywhere in the default build. With that lint green, EVERY use
// of a banned primitive in the build IS a direct call the graph sees — so the
// pair is complete for the banned set. Neither half is claimed complete alone;
// together they close direct + indirect reach.
//
// GATE C is four tests: (1) the REAL package is clean (the reach invariant
// holds); (2) the checker CATCHES a deriveKeyMaterial+bccSign DIRECT bypass
// wired into a sign entrypoint; (3) NO banned primitive is taken as a value /
// linkname-aliased in the real build, and the lint CATCHES the function-value
// indirection RED flagged; (4) — folded into (3).
import (
"go/ast"
@@ -27,6 +39,8 @@ import (
"go/token"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
)
@@ -181,7 +195,9 @@ func defaultBuildFuncFiles(t *testing.T) []*ast.File {
fset := token.NewFileSet()
files := make([]*ast.File, 0, len(pkg.GoFiles))
for _, name := range pkg.GoFiles {
f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, 0)
// ParseComments so the go:linkname scan (forbidBannedPrimitiveIndirection)
// sees directive comments; harmless for the call-graph reachability gate.
f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, parser.ParseComments)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
@@ -190,6 +206,83 @@ func defaultBuildFuncFiles(t *testing.T) []*ast.File {
return files
}
// forbidBannedPrimitiveIndirection is the companion lint that makes the
// direct-call reachability gate SOUND for the banned set (RED INFO). A name
// call-graph cannot follow a banned primitive passed as a function VALUE
// (`s := bccSign; s(...)`), captured in a closure, or aliased via
// `//go:linkname`. This lint FORBIDS exactly those: every *ast.Ident naming a
// banned primitive must be either its own declaration or the direct callee of a
// CallExpr — any other (value) use is an offender — and no `//go:linkname`
// directive may reference a banned name. With this green, every banned-primitive
// use in the build is a direct call the graph already follows.
//
// Returns the human-readable offenders (file:line — kind); empty == clean.
func forbidBannedPrimitiveIndirection(fset *token.FileSet, files []*ast.File, banned map[string]bool) []string {
var offenders []string
for _, f := range files {
// OK positions: a banned Ident that is a FuncDecl.Name (declaration) or the
// direct callee Ident of a CallExpr. Every other banned bare Ident is a
// value-use. SelectorExpr.Sel (qualified x.Name) is excluded — it is a
// different symbol than our package-local free functions.
okPos := map[token.Pos]bool{}
selPos := map[token.Pos]bool{}
for _, decl := range f.Decls {
if fn, ok := decl.(*ast.FuncDecl); ok && banned[fn.Name.Name] {
okPos[fn.Name.Pos()] = true
}
}
ast.Inspect(f, func(n ast.Node) bool {
switch e := n.(type) {
case *ast.CallExpr:
if id, ok := e.Fun.(*ast.Ident); ok && banned[id.Name] {
okPos[id.Pos()] = true // direct call: deriveKeyMaterial(...)
}
if sel, ok := e.Fun.(*ast.SelectorExpr); ok && banned[sel.Sel.Name] {
okPos[sel.Sel.Pos()] = true // qualified call: pkg.Name(...)
}
case *ast.SelectorExpr:
if banned[e.Sel.Name] {
selPos[e.Sel.Pos()] = true // qualified ref — different symbol; ignore
}
}
return true
})
ast.Inspect(f, func(n ast.Node) bool {
id, ok := n.(*ast.Ident)
if !ok || !banned[id.Name] {
return true
}
if okPos[id.Pos()] || selPos[id.Pos()] {
return true
}
pos := fset.Position(id.Pos())
offenders = append(offenders, pos.Filename+":"+strconv.Itoa(pos.Line)+" — value-use of banned primitive "+id.Name)
return true
})
// go:linkname scan: a directive whose target token is a banned primitive
// (bare `name` or qualified `importpath.name`) aliases it under a fresh
// local name the call-graph cannot follow.
for _, cg := range f.Comments {
for _, c := range cg.List {
if !strings.HasPrefix(c.Text, "//go:linkname") {
continue
}
for _, field := range strings.Fields(c.Text)[1:] { // skip the directive token
base := field
if dot := strings.LastIndexByte(field, '.'); dot >= 0 {
base = field[dot+1:]
}
if banned[base] {
pos := fset.Position(c.Pos())
offenders = append(offenders, pos.Filename+":"+strconv.Itoa(pos.Line)+" — go:linkname aliases banned primitive "+base)
}
}
}
}
}
return offenders
}
// GATE C (the invariant) — no reconstruct primitive is reachable from the
// committee sign path in the real default-build call graph.
func TestGATE_C_NoReconstructReachableFromSignPath(t *testing.T) {
@@ -290,3 +383,66 @@ func rogueReconstructSign() {
}
t.Logf("GATE C full-pipeline catch PASS: parsed rogue source flagged banned %q via %v", hit2, path2)
}
// GATE C (indirection) — RED INFO. The direct-call name graph would MISS a
// banned primitive used as a function VALUE / closure / go:linkname alias. This
// test (a) asserts the REAL default build takes no banned primitive as a value
// and aliases none via go:linkname — so the reach graph is complete for the
// banned set — and (b) proves the companion lint CATCHES the exact function-value
// indirection RED flagged, which the name graph alone does not.
func TestGATE_C_NoBannedPrimitiveValueIndirection(t *testing.T) {
// (a) the REAL default build is clean.
fset := token.NewFileSet()
pkg, err := build.Default.ImportDir(".", 0)
if err != nil {
t.Fatalf("enumerate default-build package: %v", err)
}
files := make([]*ast.File, 0, len(pkg.GoFiles))
for _, name := range pkg.GoFiles {
f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, parser.ParseComments)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
files = append(files, f)
}
if off := forbidBannedPrimitiveIndirection(fset, files, bannedReconstructPrimitives); len(off) != 0 {
t.Fatalf("GATE C (indirection) FAILED: banned primitive taken as a value / linkname-aliased in the default build:\n %v", off)
}
// (b) the lint CATCHES RED's exact function-value indirection — which the
// direct-call name graph alone does NOT flag (so the lint is load-bearing).
rogueSrc := `package pulsar
func sneaky() {
e := deriveKeyMaterial
s := bccSign
_ = e
_ = s
}
`
rfset := token.NewFileSet()
rf, err := parser.ParseFile(rfset, "sneaky.go", rogueSrc, parser.ParseComments)
if err != nil {
t.Fatalf("parse rogue source: %v", err)
}
// the name call-graph alone MISSES it (no CallExpr to the banned names) …
rg := buildCallGraph([]*ast.File{rf})
if _, _, found := rg.reachBanned([]string{"sneaky"}, bannedReconstructPrimitives); found {
t.Fatalf("unexpected: the call-graph flagged a function-VALUE use (it only follows direct calls)")
}
// … but the indirection lint CATCHES both value-uses.
if off := forbidBannedPrimitiveIndirection(rfset, []*ast.File{rf}, bannedReconstructPrimitives); len(off) < 2 {
t.Fatalf("GATE C (indirection) BROKEN: lint missed `e := deriveKeyMaterial; s := bccSign` value-use (got %v)", off)
}
// (c) go:linkname aliasing of a banned primitive is caught too.
linkSrc := "package pulsar\n\n//go:linkname myalias deriveKeyMaterial\nfunc myalias()\n"
lfset := token.NewFileSet()
lf, err := parser.ParseFile(lfset, "link.go", linkSrc, parser.ParseComments)
if err != nil {
t.Fatalf("parse linkname source: %v", err)
}
if loff := forbidBannedPrimitiveIndirection(lfset, []*ast.File{lf}, bannedReconstructPrimitives); len(loff) == 0 {
t.Fatalf("GATE C (indirection) BROKEN: lint missed go:linkname aliasing of a banned primitive")
}
t.Logf("GATE C (indirection) PASS: real build takes no banned primitive as a value/linkname; lint catches the `e:=deriveKeyMaterial; s:=bccSign` function-value indirection AND go:linkname aliasing that the name call-graph alone would miss")
}