mirror of
https://github.com/luxfi/pulsar.git
synced 2026-07-27 06:55:04 +00:00
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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user