Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions free/ci/cmd/agent/prodguard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ func TestServeStdioExitsNonZeroWithE663OnAProdStackMarker(t *testing.T) {
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
dir := t.TempDir()
// The marker guard checks for docker on PATH before calling the injected runner.
bin = t.TempDir()
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte("ENV=prod\n"), 0o600); err != nil {
t.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions free/ci/cmd/approve_pending.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package main

// List pending approvals after voiding stale approval bindings.
import (
"context"
"flag"
Expand Down
99 changes: 99 additions & 0 deletions free/ci/internal/dispatch/chokepoint_roles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package dispatch

import (
"go/ast"
"go/token"
"go/types"
"path/filepath"
"strings"
"testing"

"golang.org/x/tools/go/packages"
)

// A role Grant is unrelated to lease admission. Resolve its object rather than
// allowlisting files or functions, which could also contain a lease bypass.
func isRoleGrant(obj types.Object) bool {
if obj == nil || obj.Name() != "Grant" || obj.Pkg() == nil {
return false
}
const base = "github.com/nself-org/plugins/free/ci/internal/"
switch obj.Pkg().Path() {
case base + "store":
_, ok := obj.(*types.TypeName)
return ok
case base + "roles":
fn, ok := obj.(*types.Func)
if !ok {
return false
}
sig, ok := fn.Type().(*types.Signature)
return ok && sig.Recv() != nil
}
return false
}

func roleGrantPositions(t *testing.T, root string) map[string]map[token.Pos]bool {
t.Helper()
result := map[string]map[token.Pos]bool{}
// Planted modules deliberately contain unresolved identifiers. They retain
// the conservative syntactic scan, including their lease Grant violations.
if strings.Contains(filepath.ToSlash(root), "testdata/") {
return result
}
pkgs, err := packages.Load(&packages.Config{Dir: root,
Mode: packages.LoadSyntax}, "./...")
if err != nil {
t.Fatal(err)
}
for _, pkg := range pkgs {
if len(pkg.Errors) != 0 {
t.Fatalf("load %s: %v", pkg.PkgPath, pkg.Errors)
}
for _, objects := range []map[*ast.Ident]types.Object{pkg.TypesInfo.Uses, pkg.TypesInfo.Defs} {
for id, obj := range objects {
if !isRoleGrant(obj) {
continue
}
pos := pkg.Fset.Position(id.Pos())
rel, err := filepath.Rel(root, pos.Filename)
if err != nil {
t.Fatal(err)
}
path := filepath.Clean(filepath.Join(root, rel))
if result[path] == nil {
result[path] = map[token.Pos]bool{}
}
// scanChokepoint parses each file with a fresh FileSet (base position 1).
result[path][token.Pos(pos.Offset+1)] = true
}
}
}
return result
}

func TestRoleGrantObjects(t *testing.T) {
const base = "github.com/nself-org/plugins/free/ci/internal/"
for _, tc := range []struct {
name, pkg string
method, want bool
}{
{"role type", "store", false, true},
{"role method", "roles", true, true},
{"lease method", "lease", true, false},
{"store method", "store", true, false},
{"unrelated type", "other", false, false},
} {
t.Run(tc.name, func(t *testing.T) {
pkg := types.NewPackage(base+tc.pkg, tc.pkg)
var obj types.Object = types.NewTypeName(token.NoPos, pkg, "Grant", nil)
if tc.method {
recv := types.NewVar(token.NoPos, pkg, "s", types.Typ[types.Int])
obj = types.NewFunc(token.NoPos, pkg, "Grant", types.NewSignatureType(recv, nil, nil, nil, nil, false))
}
if got := isRoleGrant(obj); got != tc.want {
t.Fatalf("isRoleGrant = %v, want %v", got, tc.want)
}
})
}
}
14 changes: 11 additions & 3 deletions free/ci/internal/dispatch/chokepoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func ownerOf(decl ast.Decl) (string, *ast.Ident) {
}

// scanFile reports the uses in one parsed file that the allowlist does not cover.
func scanFile(f *ast.File, rel string, add func(string)) {
func scanFile(f *ast.File, rel string, grants map[token.Pos]bool, add func(string)) {
dir := filepath.ToSlash(filepath.Dir(rel))
usesReflect := false
for _, imp := range f.Imports {
Expand All @@ -108,6 +108,9 @@ func scanFile(f *ast.File, rel string, add func(string)) {
switch x := n.(type) {
case *ast.SelectorExpr:
name := x.Sel.Name
if grants[x.Sel.Pos()] {
return true
}
switch {
case reflective[name], name == "Method" && usesReflect:
report(name)
Expand All @@ -119,7 +122,7 @@ func scanFile(f *ast.File, rel string, add func(string)) {
report(name)
}
case *ast.Ident:
if banned[x.Name] && x != declName && !(x.Name == "Grant" && roleGrantFiles[rel]) && !(x.Name == "LocalAdmit" && localFiles[rel]) && (x.Name == "LocalAdmit" || !chokeFuncs[key]) {
if banned[x.Name] && !grants[x.Pos()] && x != declName && (x.Name != "LocalAdmit" || !localFiles[rel]) && (x.Name == "LocalAdmit" || !chokeFuncs[key]) {
report(x.Name)
}
case *ast.BasicLit:
Expand All @@ -136,6 +139,11 @@ func scanFile(f *ast.File, rel string, add func(string)) {
// allowlist, and how many files it read.
func scanChokepoint(t *testing.T, root string) (violations []string, files int) {
t.Helper()
root, absErr := filepath.Abs(root)
if absErr != nil {
t.Fatal(absErr)
}
roleGrants := roleGrantPositions(t, root)
seen := map[string]bool{}
add := func(v string) { // a selector and its identifier are two nodes
if !seen[v] {
Expand Down Expand Up @@ -163,7 +171,7 @@ func scanChokepoint(t *testing.T, root string) (violations []string, files int)
t.Fatalf("parse %s: %v", path, perr)
}
files++
scanFile(f, filepath.ToSlash(rel), add)
scanFile(f, filepath.ToSlash(rel), roleGrants[filepath.Clean(path)], add)
return nil
})
if err != nil {
Expand Down
8 changes: 7 additions & 1 deletion free/ci/internal/gates/node_prepare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,15 @@ type result struct {
func (h *nodeHost) run(id, workdir string) result {
h.t.Helper()
p := h.plan(id, workdir)
// macOS temp roots can include /var -> /private/var. The executor rejects
// symlinked workdirs, so use the real fixture home just as a configured CI home.
home, err := filepath.EvalSymlinks(h.t.TempDir())
if err != nil {
h.t.Fatal(err)
}
var out strings.Builder
res := (&ciexec.Executor{Isolation: ciexec.BaselineIsolator{}}).Run(context.Background(), ciexec.JobSpec{AttemptID: "node-" + strings.ReplaceAll(id, ".", "-"), CoordinatorID: "coord",
Secrets: h.secrets, Job: model.Job{Kind: "quality", Workdir: workdir, Isolation: "process", Env: h.env, Secrets: h.refs}, Command: p.Command(), SourceDir: h.root, Home: realTempDir(h.t),
Secrets: h.secrets, Job: model.Job{Kind: "quality", Workdir: workdir, Isolation: "process", Env: h.env, Secrets: h.refs}, Command: p.Command(), SourceDir: h.root, Home: home,
Output: func(b []byte) { out.Write(b) }})
r := result{exit: res.ExitCode, out: strings.TrimSpace(out.String())}
if res.Err != nil {
Expand Down
4 changes: 2 additions & 2 deletions free/ci/internal/protect/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ func TestHostedSourceWorkflowTrust(t *testing.T) {
file, job, command, runner string
calls int
}{
{"../../.github/workflows/nself-ci-source.yml", "nself-ci-source", `"$NSELF_CI_BIN"`, "ubuntu-latest", 1},
{"../../.github/workflows/nself-ci-source.yml", "nself-ci-source", `"$NSELF_CI_BIN"`, "ubuntu-latest", 1},
{"../../.github/workflows/ci-scenarios.yml", "scenarios", ".drill-bin/nself-ci", "${{ matrix.os }}", 2},
} {
} {
t.Run(tc.job, func(t *testing.T) {
d, raw := readWorkflow(t, tc.file)
job, ok := d.Jobs[tc.job]
Expand Down
4 changes: 3 additions & 1 deletion free/ci/internal/run/isolation_wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ func TestRunWaitingStopsOnCancel(t *testing.T) {
rs, spec := waitRun(t, iso)
rs.sess.slots.take(slotDir(rs.sess.Home)) // slot 0 is held elsewhere, so the attempt waits on slot 1
ctx, cancel := context.WithCancel(context.Background())
time.AfterFunc(50*time.Millisecond, cancel)
defer cancel()
// Cancel once the attempt enters its wait, independently of executor setup time.
spec.Output = func([]byte) { cancel() }
start := time.Now()
res := rs.runWaiting(ctx, spec)
if string(res.Attempt.State) != "cancelled" || time.Since(start) > 5*time.Second {
Expand Down
Loading