Bind a run's heads read-only over its workspace, read its input from the state the ask names, and prove the kept programs' sweep (issue 1921, review round 1)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 23:24:48 +01:00
parent fa73674e22
commit da1356cd7c
7 changed files with 64 additions and 40 deletions
+14 -8
View File
@@ -27,16 +27,19 @@ const Bubblewrap = "bwrap"
// - the system's programs and libraries read-only, and of /etc only what a program needs to run (users, libraries,
// certificates, time zone), with a hosts file naming only the loopback;
// - the Workspace and Writable directories read-write, the ReadOnly ones (the toolchain, the modules the fetch step
// downloaded) read-only;
// downloaded) read-only, and the ReadOnlyInside ones (inside the Workspace) read-only over it;
// - the shared warm Caches read-only under a throwaway writable layer (S18), or read-write for a trunk build alone
// (WriteCaches), so a candidate's code never changes what a later build reads.
type Isolation struct {
Workspace string
Writable []string
ReadOnly []string
Caches []string
WriteCaches bool
Env []string
Workspace string
Writable []string
ReadOnly []string
// ReadOnlyInside are directories inside the Workspace or a Writable one bound read-only after them, so they stay
// read-only (a later bind of a path wins; a ReadOnly one inside the Workspace would be covered by it).
ReadOnlyInside []string
Caches []string
WriteCaches bool
Env []string
// Hosts is the file bound as /etc/hosts; Isolated writes one naming only the loopback.
Hosts string
}
@@ -91,7 +94,7 @@ func (iso Isolation) command(env []string, name string, args ...string) ([]strin
for _, group := range []struct {
what string
dirs []string
}{{"writable directory", iso.Writable}, {"read-only directory", iso.ReadOnly}, {"cache", iso.Caches}} {
}{{"writable directory", iso.Writable}, {"read-only directory", iso.ReadOnly}, {"read-only directory", iso.ReadOnlyInside}, {"cache", iso.Caches}} {
for _, d := range group.dirs {
if err := checkDir(group.what, d); err != nil {
return nil, err
@@ -118,6 +121,9 @@ func (iso Isolation) command(env []string, name string, args ...string) ([]strin
for _, d := range iso.Writable {
argv = append(argv, "--bind", d, d)
}
for _, d := range iso.ReadOnlyInside {
argv = append(argv, "--ro-bind", d, d)
}
if iso.Hosts != "" {
argv = append(argv, "--ro-bind", iso.Hosts, "/etc/hosts")
}
@@ -31,7 +31,7 @@ func toolOf(t *testing.T, m *Module, name string) func(map[string]any) (any, err
}
// Seams review must 2 (issue 1921): the kept composers and programs are listed with their size and last use, dropped one
// by one, and swept when unused past KeptBound; a use keeps a program.
// by one, and swept with the workspaces when unused past KeptBound; a use keeps a program.
func TestKeptProgramsAreListedDroppedAndSwept(t *testing.T) {
m, _ := fetchModule(t, &fakeStep{})
now := time.Now().UTC().Truncate(time.Second)
@@ -62,9 +62,8 @@ func TestKeptProgramsAreListedDroppedAndSwept(t *testing.T) {
t.Error("the dropped program is still kept")
}
if swept := m.SweepKept(now, KeptBound); len(swept) != 1 || swept[0].Key != "old" {
t.Fatalf("swept %+v, want the old composer only", swept)
}
// The sweep runs with the workspaces' (CleanWorkspaces, on its timer).
m.CleanWorkspaces(now)
if got := m.Kept(); len(got) != 1 || got[0].Key != "recent" {
t.Fatalf("kept after the sweep = %+v", got)
}
@@ -7,6 +7,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/user"
@@ -58,7 +59,7 @@ func main() {
m.Settings = m.Home + "/settings.json"
m.ReadKept = readOf(stdio.State(KeptState)).Get
m.ReadManifest = readOf(stdio.State(ManifestsState)).Get
m.ReadInput = readOf(stdio.State(InputsState)).Get
m.ReadInput = func(state, key string) (json.RawMessage, error) { return readOf(stdio.State(state)).Get(key) }
m.Probe = ProbeGo(m.Home, m.Cache)
m.Bubblewrap = ProbeBubblewrap
// Outside a launch by the runtime — a build, a check — there is no node and no state to write.
@@ -93,8 +93,8 @@ type Module struct {
// ReadManifest reads a key of the delivery indexer's manifests state (ManifestsState), which a compose ask's request
// names its manifests by (compose.go); GoModCache, when set, is the Go module cache in place of Cache's (a test).
ReadManifest func(key string) (json.RawMessage, error)
// ReadInput reads a key of the checker's inputs state (InputsState), which a run ask's input names by its digest.
ReadInput func(key string) (json.RawMessage, error)
// ReadInput reads a key of a state the module reads (module.json), where a run ask's input names its digest.
ReadInput func(state, key string) (json.RawMessage, error)
// PlaceHead, when set, lays a run's head at a directory instead of fetching it (a test).
PlaceHead func(ctx context.Context, h delivery.RunHead, dir string) error
GoModCache string
+6 -12
View File
@@ -4,7 +4,7 @@ package main
// checking program of a group's check. The module builds the program at the commit the ask names, once per
// delivery.RunKey, as a compose ask builds a composer (programOf); fetches every head the ask names and lays each read-only
// under the directory MESH_RUN_HEADS names, at `<owner>/<name>`; reads the input the ask names by its digest from the
// checker's inputs state and writes it to the file MESH_RUN_INPUT names; and runs the program shut off with the ask's
// state the ask names and writes it to the file MESH_RUN_INPUT names; and runs the program shut off with the ask's
// arguments: no network, none of the account. The answer is delivery.RunAnswer: the program's exit, the last line of its
// standard output and the first line of its standard error. A program that fails is an answer, not a fault of the step.
@@ -22,15 +22,9 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// InputsState is the checker's state that keeps a run's input by its digest (the module reads it, module.json reads).
const InputsState = delivery.CheckerModule + ".inputs"
// run runs a run ask: the program of its commit, built once, over its heads and its input.
func (m *Module) run(ctx context.Context, fetch buildstep.StepFunc, a delivery.StepAsk) (any, error) {
job := *a.Run
if job.Input.State != InputsState {
return nil, buildstep.Refuse("the run ask %s keeps its input in %s; this module reads %s", a.ID, job.Input.State, InputsState)
}
body, err := m.inputOf(job.Input)
if err != nil {
return nil, err
@@ -53,17 +47,18 @@ func (m *Module) run(ctx context.Context, fetch buildstep.StepFunc, a delivery.S
return ans, nil
}
// inputOf is the input a run names, read from the checker's inputs state and checked against its digest and size.
// inputOf is the input a run names, read from the state the ask names (module.json reads it) and checked against its
// digest and size.
func (m *Module) inputOf(in delivery.RunInput) ([]byte, error) {
if m.ReadInput == nil {
return nil, errors.New("this module reads no inputs state")
}
raw, err := m.ReadInput(in.Digest)
raw, err := m.ReadInput(in.State, in.Digest)
if err != nil {
return nil, err
}
if raw == nil {
return nil, fmt.Errorf("the checker keeps no input of digest %s", in.Digest)
return nil, fmt.Errorf("%s keeps no input of digest %s", in.State, in.Digest)
}
if err := in.Verify(raw); err != nil {
return nil, buildstep.Refuse("%v", err)
@@ -115,10 +110,9 @@ func (m *Module) runProgram(ctx context.Context, exe string, job delivery.RunJob
if !m.Inside {
iso := m.isolation(id, false)
iso.ReadOnly = append(iso.ReadOnly, exe)
iso.ReadOnlyInside = []string{heads} // bound after the workspace, so the program cannot change a head
run = buildstep.Isolated(m.Run, iso)
}
// The heads are bound read-only inside the writable workspace by running the program with them as its read-only
// directory too (a later bind of the same path wins).
r := run(ctx, []string{delivery.RunHeadsEnv + "=" + heads, delivery.RunInputEnv + "=" + file}, exe, job.Args...)
if r.Err != "" && r.Status == 0 {
return delivery.RunAnswer{}, fmt.Errorf("the checking program could not run: %s", r.Err)
@@ -22,8 +22,8 @@ import (
// 1921/C3: a run ask put on the Go build seat's check stream is pulled by the module started whole on the real bus and
// answered on its step key <node>.<ask id>: the program, run shut off over its heads and its input (read from the
// checker's inputs state by digest), answers its last line and exit; a program that dials the machine's network is
// shut off, and the answer says it failed.
// state the ask names by digest), answers its last line and exit; a program cannot write under its heads, and one
// that dials the machine's network is shut off, and the answer says it failed.
func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) {
needTools(t, "the shut-off run", "bash", "bwrap")
nc, err := nats.Connect(testbus.URL(t))
@@ -59,13 +59,13 @@ func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) {
}
m.PlaceHead = func(_ context.Context, h delivery.RunHead, dir string) error { return os.MkdirAll(dir, 0o700) }
body := `{"v":1}`
m.ReadInput = func(string) (json.RawMessage, error) { return json.RawMessage(body), nil }
m.ReadInput = func(string, string) (json.RawMessage, error) { return json.RawMessage(body), nil }
a := runAskOf(t, body)
key := delivery.RunKey(a.Run.Program, delivery.ToolchainUsed{Version: "go1.26.0", Recipe: m.Recipe.Version()}, nil)
exe := m.programPath("programs", key)
os.MkdirAll(filepath.Dir(exe), 0o700)
script := "#!/bin/bash\n" + `echo "start"; exec 3<>/dev/tcp/127.0.0.1/` + strings.Split(tcp.Addr().String(), ":")[1] + " || exit 7\n" + `echo '{"dialed":true}'` + "\n"
script := "#!/bin/bash\n" + `if touch "$MESH_RUN_HEADS/novox/mesh-sdk/x" 2>/dev/null; then echo '{"wrote":true}'; exit 9; fi; echo "start"; exec 3<>/dev/tcp/127.0.0.1/` + strings.Split(tcp.Addr().String(), ":")[1] + " || exit 7\n" + `echo '{"dialed":true}'` + "\n"
if err := os.WriteFile(exe, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
@@ -12,13 +12,17 @@ import (
sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
const runCommit = "5555555555555555555555555555555555555555"
// runInputs is the state a run ask's input is kept in, as the checker names it in the ask.
const runInputs = "mesh-delivery-checker.inputs"
func runAskOf(t *testing.T, body string) delivery.StepAsk {
t.Helper()
in, err := delivery.RunInputOf(InputsState, []byte(body))
in, err := delivery.RunInputOf(runInputs, []byte(body))
if err != nil {
t.Fatal(err)
}
@@ -35,8 +39,8 @@ func runAskOf(t *testing.T, body string) delivery.StepAsk {
func TestAKeptProgramIsRunOverItsHeadsAndInput(t *testing.T) {
m := composeModule(t)
body := `{"v":1,"case":"x"}`
m.ReadInput = func(key string) (json.RawMessage, error) {
if key != runAskOf(t, body).Run.Input.Digest {
m.ReadInput = func(state, key string) (json.RawMessage, error) {
if state != runInputs || key != runAskOf(t, body).Run.Input.Digest {
return nil, nil
}
return json.RawMessage(body), nil
@@ -68,15 +72,35 @@ func TestAKeptProgramIsRunOverItsHeadsAndInput(t *testing.T) {
if entries, _ := os.ReadDir(m.Work); len(entries) != 0 {
t.Errorf("the run's workspace was left: %v", entries)
}
// An input other than the one named is refused, naming it; an unknown state is refused.
m.ReadInput = func(string) (json.RawMessage, error) { return json.RawMessage(`{"other":1}`), nil }
// Shut off, the heads are bound read-only after the writable workspace, so a write under MESH_RUN_HEADS fails: a
// read-only bind before the workspace's is covered by it.
m.Inside = false
var argv []string
m.Run = func(_ context.Context, _ []string, _ string, args ...string) forgekey.Ran {
argv = args
return forgekey.Ran{Status: 0, Stdout: "{}"}
}
if _, err := m.run(context.Background(), fetch, a); err != nil {
t.Fatal(err)
}
line, ws, heads := " "+strings.Join(argv, " ")+" ", -1, -1
for i := 0; i+2 < len(argv); i++ {
switch {
case argv[i] == "--bind" && filepath.Dir(argv[i+1]) == m.Work:
ws = i
case argv[i] == "--ro-bind" && filepath.Base(argv[i+1]) == "heads" && argv[i+2] == argv[i+1]:
heads = i
}
}
if ws < 0 || heads < ws || !strings.Contains(line, " "+delivery.RunHeadsEnv+" "+argv[heads+1]+" ") {
t.Errorf("the heads are not bound read-only over the workspace: %s", line)
}
m.Inside, m.Run = true, StepRunner(t.TempDir())
// An input other than the one named is refused, naming it.
m.ReadInput = func(string, string) (json.RawMessage, error) { return json.RawMessage(`{"other":1}`), nil }
if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "not the one the ask names") {
t.Errorf("a wrong input: %v", err)
}
a.Run.Input.State = "mesh-delivery-indexer.manifests"
if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "this module reads") {
t.Errorf("another state: %v", err)
}
}
// 1921/C2 (F2): a compose ask's seats reach the composer: the module decodes and fills the request and writes it again,