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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user