Review of 62a50eb: the layer lay in the workspace, which every step binds read-write,
so a change's command could swap a kept directory for a symlink and the move, run
outside the sandbox, would write into the shared cache (ADR 0541 §2). The layer now
lies beside the workspace (Workspaces.CacheLayer, removed by Release and Sweep),
Command refuses a layer within reach of the workspace, a writable directory or a
cache, and every move goes through an os.Root on the layer and keeps no symlink. Only
LayerCaches (the build cache) are layered; a module cache keeps its throwaway layer
(issue 1921).
100 lines
5.2 KiB
Go
100 lines
5.2 KiB
Go
//go:build integration && isolation
|
|
|
|
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)
|
|
}
|
|
|
|
// 1921/S2: with the ask's cache layer, what one command writes into the cache the next command of the ask reads,
|
|
// above what the shared cache holds, and the shared cache stays unchanged.
|
|
layer := filepath.Join(filepath.Dir(workspace), "ws1.cachelayer")
|
|
asked := Isolated(forgekey.ExecRunnerFor(0), Isolation{Workspace: workspace, Caches: []string{cacheDir},
|
|
CacheLayer: layer, LayerCaches: []string{cacheDir}})
|
|
r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/sh", "-c", "cat "+filepath.Join(cacheDir, "warm")+" && mkdir -p "+filepath.Join(cacheDir, "ab")+" && echo compiled > "+filepath.Join(cacheDir, "ab", "one-d"))
|
|
if r.Status != 0 || r.Err != "" || strings.TrimSpace(r.Stdout) != "warm" {
|
|
t.Fatalf("the first command of the ask did not read the shared cache and write its own (status %d, %s): %s%s", r.Status, r.Err, r.Stdout, r.Stderr)
|
|
}
|
|
r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/cat", filepath.Join(cacheDir, "ab", "one-d"), filepath.Join(cacheDir, "warm"))
|
|
if r.Status != 0 || r.Err != "" || r.Stdout != "compiled\nwarm\n" {
|
|
t.Fatalf("the next command of the ask did not find what the first wrote (status %d, %s): %q %s", r.Status, r.Err, r.Stdout, r.Stderr)
|
|
}
|
|
// A command cannot reach the layer (it lies outside every directory the step binds), and a symlink it writes into
|
|
// the cache is not kept, so nothing it does makes the keeping write outside the layer.
|
|
r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/sh", "-c",
|
|
"ls "+layer+" >/dev/null 2>&1 && echo reached; ln -s "+cacheDir+" "+filepath.Join(cacheDir, "esc")+" && mkdir "+filepath.Join(cacheDir, "cd")+" && echo x > "+filepath.Join(cacheDir, "cd", "f"))
|
|
if r.Status != 0 || r.Err != "" || strings.Contains(r.Stdout, "reached") {
|
|
t.Fatalf("the command reached the cache layer or could not write the cache (status %d, %s): %q %s", r.Status, r.Err, r.Stdout, r.Stderr)
|
|
}
|
|
if fi, err := os.Lstat(filepath.Join(keptLayer(layer, 0), "esc")); err == nil {
|
|
t.Fatalf("a symlink the command wrote was kept: %v", fi.Mode())
|
|
}
|
|
if b, err := os.ReadFile(filepath.Join(keptLayer(layer, 0), "cd", "f")); err != nil || string(b) != "x\n" {
|
|
t.Fatalf("the command's file was not kept: %q %v", b, err)
|
|
}
|
|
if entries, _ := os.ReadDir(cacheDir); len(entries) != 1 || entries[0].Name() != "warm" {
|
|
t.Fatalf("the ask's commands wrote the shared cache: %v", entries)
|
|
}
|
|
if err := os.RemoveAll(layer); err != nil {
|
|
t.Fatalf("the ask's cache layer cannot be removed: %v", err)
|
|
}
|
|
}
|