bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
// console_test.go — every child process this daemon starts on Windows must be
|
||||
// started with no console window.
|
||||
//
|
||||
// WHY A SOURCE-LEVEL TEST. The defect is invisible on the platform the tests run
|
||||
// on: on Linux there is no console to allocate, so a behavioural test of
|
||||
// hideConsole passes vacuously here and the real thing can only be seen by a
|
||||
// person looking at a Windows desktop. What CAN be checked anywhere is the
|
||||
// property that actually matters — that no exec site was left out. The last
|
||||
// one was found by watching two blue windows flash every ten minutes
|
||||
// (console_windows.go); this is what stops the next shell-out doing it again.
|
||||
//
|
||||
// It asks the toolchain which files are in a Windows build (go/build's own
|
||||
// MatchFile, as buildtags_test.go does) rather than reading name suffixes, so a
|
||||
// file behind any constraint form is judged correctly.
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// exemptFromHideConsole: files whose exec sites are deliberately different.
|
||||
var exemptFromHideConsole = map[string]string{
|
||||
// The self-update relaunch MUST keep DETACHED_PROCESS — that flag is the
|
||||
// whole point, it is what lets the replacement outlive the process being
|
||||
// replaced. It has no console by construction and starts nothing visible.
|
||||
"relaunch_windows.go": "sets DETACHED_PROCESS deliberately",
|
||||
// The approvals popup starts Edge itself: a GUI program with no console to
|
||||
// hide, where hideConsole's HideWindow (SW_HIDE in the start-up info) would
|
||||
// ask Edge to open its first window hidden — the popup, invisible. Its one
|
||||
// exec site is the Edge launch; the browser fallback goes through
|
||||
// openBrowser, which hides rundll32's console as every other site does.
|
||||
"uipopup_windows.go": "starts a GUI window that must be seen",
|
||||
}
|
||||
|
||||
// isExecCommand reports whether a call is exec.Command or exec.CommandContext.
|
||||
func isExecCommand(call *ast.CallExpr) bool {
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
pkg, ok := sel.X.(*ast.Ident)
|
||||
if !ok || pkg.Name != "exec" {
|
||||
return false
|
||||
}
|
||||
return sel.Sel.Name == "Command" || sel.Sel.Name == "CommandContext"
|
||||
}
|
||||
|
||||
// SCOPE, because a guard is only as wide as its walk:
|
||||
// this reads THIS directory only. The bastion is a single-package module today
|
||||
// — `go list ./...` is one package — so that is every exec site there is. The
|
||||
// day a subpackage appears, its shell-outs are invisible here and this check
|
||||
// goes on passing while the thing it protects stops being true. Widen the walk
|
||||
// then; do not assume the walk widened itself.
|
||||
func TestEveryWindowsExecSiteHidesItsConsole(t *testing.T) {
|
||||
entries, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
win := build.Default
|
||||
win.GOOS = "windows"
|
||||
|
||||
checked := 0
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||||
continue
|
||||
}
|
||||
if _, skip := exemptFromHideConsole[name]; skip {
|
||||
continue
|
||||
}
|
||||
onWin, err := win.MatchFile(".", name)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if !onWin {
|
||||
continue // never compiled for Windows; there is no window to hide
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, name, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
|
||||
// One top-level declaration at a time, so a `var x = func(){…}` (as
|
||||
// observe_ops.go uses) is one scope and the name match stays local.
|
||||
for _, decl := range f.Decls {
|
||||
var bound []struct {
|
||||
name string
|
||||
line int
|
||||
}
|
||||
hidden := map[string]bool{}
|
||||
|
||||
ast.Inspect(decl, func(n ast.Node) bool {
|
||||
switch node := n.(type) {
|
||||
case *ast.AssignStmt:
|
||||
// x := exec.Command(…) — the shape that CAN be hidden.
|
||||
if len(node.Rhs) == 1 && len(node.Lhs) >= 1 {
|
||||
if call, ok := node.Rhs[0].(*ast.CallExpr); ok && isExecCommand(call) {
|
||||
if id, ok := node.Lhs[0].(*ast.Ident); ok {
|
||||
bound = append(bound, struct {
|
||||
name string
|
||||
line int
|
||||
}{id.Name, fset.Position(node.Pos()).Line})
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.CallExpr:
|
||||
// hideConsole(x)
|
||||
if id, ok := node.Fun.(*ast.Ident); ok && id.Name == "hideConsole" {
|
||||
if len(node.Args) == 1 {
|
||||
if arg, ok := node.Args[0].(*ast.Ident); ok {
|
||||
hidden[arg.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// exec.Command(…).Output() — nothing is bound, so nothing
|
||||
// can be hidden. Must be split into a variable first.
|
||||
if sel, ok := node.Fun.(*ast.SelectorExpr); ok {
|
||||
if inner, ok := sel.X.(*ast.CallExpr); ok && isExecCommand(inner) {
|
||||
t.Errorf("%s:%d — exec.Command(…).%s() is started inline, so no "+
|
||||
"hideConsole can be applied and it opens a console window on "+
|
||||
"Windows. Assign it first:\n"+
|
||||
"\tcmd := exec.CommandContext(…)\n\thideConsole(cmd)\n\t… cmd.%s()",
|
||||
name, fset.Position(node.Pos()).Line, sel.Sel.Name, sel.Sel.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, b := range bound {
|
||||
checked++
|
||||
if !hidden[b.name] {
|
||||
t.Errorf("%s:%d — %s is an exec.Cmd that is never passed to hideConsole, "+
|
||||
"so on Windows it opens a console window on the user's desktop every "+
|
||||
"time it runs. Add `hideConsole(%s)` before it is started (and AFTER "+
|
||||
"any assignment to %s.SysProcAttr, which would otherwise replace the "+
|
||||
"flags). See console_windows.go.",
|
||||
name, b.line, b.name, b.name, b.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A guard that checked nothing would pass for ever after a refactor moved
|
||||
// every exec site behind a helper this walk does not recognise.
|
||||
if checked < 8 {
|
||||
t.Fatalf("only %d exec site(s) examined — this check has stopped finding them, "+
|
||||
"which makes it a green light that proves nothing", checked)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user