Files
privatae-bastion/client-go/agents_exe_linux_test.go
T
Privatae e4c9b69b46
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s
bastion 0.53.2
2026-10-20 04:42:14 +00:00

121 lines
4.2 KiB
Go

//go:build linux
package main
// procExe against REAL processes on this machine (agents_linux.go).
//
// Everything else about the exe identity is proven with fixtures, which is the
// right way to test a rule. But the CLAIM underneath the whole change is about
// what an operating system does — "argv[0] is chosen by the launcher, the
// executable path is not" — and a fixture cannot establish that. These two
// tests ask the kernel.
//
// Linux only, and that is honest rather than a gap: this is the platform the
// test runner is on, and a claim about macOS or Windows behaviour must not be
// asserted by a Linux box. Their paths are exercised through
// parseProcargs2ExePath and the offsets in agents_windows.go.
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// The simplest true statement available: this test binary's own executable.
// No fixture, no stub — if procExe cannot answer for a process that is
// certainly running, nothing else in this file means anything.
//
// MUTATION: read /proc/<pid>/cmdline instead of /proc/<pid>/exe in procExe.
// RESULT: red — the answer became the go test command line, which is not a
// path to anything.
func TestProcExeAnswersTheKernelForThisProcess(t *testing.T) {
self, err := os.Executable()
if err != nil {
t.Skipf("this machine cannot name its own executable: %v", err)
}
got := procExe(os.Getpid())
if got == "" {
t.Fatal("procExe could not read this process's own executable")
}
if got != self {
t.Errorf("procExe(self) = %q, want %q (os.Executable)", got, self)
}
if !filepath.IsAbs(got) {
t.Errorf("procExe returned a relative path %q — sanitizeExePath must have dropped it", got)
}
}
// THE MEASUREMENT THE WHOLE CHANGE RESTS ON, run against the kernel rather
// than quoted from a comment: a process can present any argv[0] it likes, and
// /proc/<pid>/exe still names the file that was actually loaded.
//
// This is the forgery TestAForgedArgv0FromADifferentExecutableIsNotSanctioned
// (sanction_test.go) refuses, performed for real. If this test ever fails on a
// platform, the identity in sanction.go is not worth what it claims there and
// the header's cost analysis has to be rewritten.
//
// MUTATION: return sanitizeExePath(procCmdline(pid)) from procExe.
// RESULT: red — exe came back as the forged /tmp/.../claude, matching argv[0]
// exactly, which is the defect this exists to catch.
func TestProcExeIgnoresAForgedArgv0(t *testing.T) {
bash, err := exec.LookPath("bash")
if err != nil {
t.Skipf("no bash on this machine: %v", err)
}
sleepBin, err := exec.LookPath("sleep")
if err != nil {
t.Skipf("no sleep on this machine: %v", err)
}
// A path that does not exist, deliberately: the forgery needs no file at
// all, which is exactly what made argv[0] worthless as an identity.
const claimed = "/home/user/.local/bin/claude"
cmd := exec.Command(bash, "-c", "exec -a "+claimed+" "+sleepBin+" 30")
if err := cmd.Start(); err != nil {
t.Fatalf("starting the forger: %v", err)
}
pid := cmd.Process.Pid
t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
// bash execs into sleep in place, keeping the pid. Wait for that to happen
// rather than sleeping a fixed amount.
var cmdline, exe string
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
cmdline, exe = procCmdline(pid), procExe(pid)
if strings.HasPrefix(cmdline, claimed) {
break
}
time.Sleep(20 * time.Millisecond)
}
if !strings.HasPrefix(cmdline, claimed) {
t.Fatalf("the forger never took its new name: cmdline=%q", cmdline)
}
// What the process SAYS it is.
if got := argv0(cmdline); got != claimed {
t.Fatalf("argv[0] = %q, want the forged %q", got, claimed)
}
// What the KERNEL says it is.
if exe == "" {
t.Fatal("procExe read nothing for a process this test started itself")
}
if exe == claimed {
t.Fatalf("procExe returned the forged path %q — it is reading the command "+
"line, not /proc/<pid>/exe, and every identity in sanction.go built on "+
"it is forgeable by one exec", exe)
}
if filepath.Base(exe) != "sleep" {
t.Errorf("procExe = %q, want the sleep binary that is actually running", exe)
}
if _, err := os.Stat(exe); err != nil {
t.Errorf("procExe named %q, which is not a file on this machine: %v", exe, err)
}
}