Prove a run step reaches no key, login, name or socket and leaves the caches unchanged (issue 1309, S19, ADR 0448 check)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
//go:build integration
|
||||
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// 1309/S19, the issue's proof: a run step, in bubblewrap as Isolated runs it, reads no key and no login, resolves no
|
||||
// name, reaches no TCP or unix socket of the machine, finds no home of the account, and leaves the shared cache it
|
||||
// wrote unchanged; the workspace it writes keeps what it wrote. It needs bubblewrap and unprivileged user namespaces
|
||||
// (issue 1308) on the machine that runs it, and fails naming them when they are not there.
|
||||
func TestARunStepReachesNothingAndLeavesTheCachesUnchanged(t *testing.T) {
|
||||
if _, err := exec.LookPath(Bubblewrap); err != nil {
|
||||
t.Fatalf("bubblewrap is not installed here, so the isolation of a run step cannot be proved (issue 1309): %v", err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
account := filepath.Join(root, "account")
|
||||
key, login := filepath.Join(account, ".ssh", "id_ed25519"), filepath.Join(account, "bus-login.json")
|
||||
writeFile(t, filepath.Dir(key), filepath.Base(key), "PRIVATE KEY")
|
||||
writeFile(t, account, "bus-login.json", `{"password":"secret"}`)
|
||||
cacheDir := filepath.Join(root, "cache")
|
||||
writeFile(t, cacheDir, "warm", "warm\n")
|
||||
workspace := filepath.Join(root, "work", "tree")
|
||||
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
probe := filepath.Join(workspace, "probe")
|
||||
if out, err := exec.Command("go", "build", "-o", probe, "./testdata/isolated-probe").CombinedOutput(); err != nil {
|
||||
t.Fatalf("building the probe: %v\n%s", err, out)
|
||||
}
|
||||
tcp, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tcp.Close()
|
||||
socket := filepath.Join(account, "bus.sock")
|
||||
unix, err := net.Listen("unix", socket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Close()
|
||||
|
||||
run := Isolated(forgekey.ExecRunnerFor(0), Isolation{Workspace: workspace, Caches: []string{cacheDir}})
|
||||
r := run(context.Background(), []string{"PATH=/usr/bin:/bin"}, probe, key, login, tcp.Addr().String(), socket, "localhost.localdomain.invalid", filepath.Join(cacheDir, "warm"))
|
||||
if r.Status != 0 || r.Err != "" || strings.TrimSpace(r.Stdout) != "shut off" {
|
||||
t.Fatalf("the run step was not shut off (status %d, %s):\n%s%s", r.Status, r.Err, r.Stdout, r.Stderr)
|
||||
}
|
||||
if b, _ := os.ReadFile(filepath.Join(cacheDir, "warm")); string(b) != "warm\n" {
|
||||
t.Fatalf("the step changed the shared cache: %q", b)
|
||||
}
|
||||
|
||||
// The workspace is the step's own: what it writes there stays.
|
||||
r = run(context.Background(), nil, "/bin/sh", "-c", "echo built > out")
|
||||
if b, err := os.ReadFile(filepath.Join(workspace, "out")); r.Status != 0 || err != nil || string(b) != "built\n" {
|
||||
t.Fatalf("the step's write to its workspace did not stay (status %d, %s%s): %q %v", r.Status, r.Stderr, r.Err, b, err)
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// The probe a run step's isolation is proved with (novox/hq ADR 0448; issue 1309, S19): run inside the step, it says
|
||||
// on stdout every way it reached what a step must not, and "shut off" when it reached none. Its arguments: the files
|
||||
// it must not read (the forge key, a bus login), a TCP address and a unix socket of the machine it must not reach, a
|
||||
// name it must not resolve, and a cache file it writes, which must be unchanged after the step.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 7 {
|
||||
fmt.Println("usage: probe <key> <login> <tcp address> <unix socket> <name> <cache file>")
|
||||
os.Exit(2)
|
||||
}
|
||||
key, login, tcp, socket, name, cache := os.Args[1], os.Args[2], os.Args[3], os.Args[4], os.Args[5], os.Args[6]
|
||||
reached := 0
|
||||
say := func(f string, a ...any) { reached++; fmt.Printf(f+"\n", a...) }
|
||||
for _, f := range []string{key, login} {
|
||||
if _, err := os.ReadFile(f); err == nil {
|
||||
say("read %s", f)
|
||||
}
|
||||
}
|
||||
if c, err := net.DialTimeout("tcp", tcp, 2*time.Second); err == nil {
|
||||
c.Close()
|
||||
say("reached %s", tcp)
|
||||
}
|
||||
if c, err := net.DialTimeout("unix", socket, 2*time.Second); err == nil {
|
||||
c.Close()
|
||||
say("reached %s", socket)
|
||||
}
|
||||
if addrs, err := net.LookupHost(name); err == nil {
|
||||
say("resolved %s to %v", name, addrs)
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
if entries, err := os.ReadDir(home); err == nil && len(entries) > 0 {
|
||||
say("the home %s holds %d entries", home, len(entries))
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(cache, []byte("written by the step\n"), 0o644); err != nil {
|
||||
say("could not write the cache under its throwaway layer: %v", err)
|
||||
}
|
||||
if reached == 0 {
|
||||
fmt.Println("shut off")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user