carlos / platform Public

instanceexec: the trampoline reads the launch file through instancefs and execs the live link (plan 2, task 2)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev f7cd33d85499ffeb0360cba2f08e926a701e3232 parent cdc34b8
2 files changed, +223 −0
  • internal/instanceexec/exec.go +105 −0
  • internal/instanceexec/exec_test.go +118 −0
diff --git a/internal/instanceexec/exec.go b/internal/instanceexec/exec.go
new file mode 100644
index 000000000..dd89bdf85
--- /dev/null
+++ b/internal/instanceexec/exec.go
@@ -0,0 +1,105 @@
+// Package instanceexec holds the three programs that run INSIDE an isolated
+// instance's namespace, as carlos, launched by the template units of the
+// isolated-exec design: the trampoline (instance-exec, §6.2), the advisory
+// exit-record writer (instance-exit, §6.5) and the readiness probe
+// (instance-probe, §8.5). None of them is for operators; each is an
+// ExecStart= or ExecStopPost= of a unit.
+package instanceexec
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "github.com/carlosframework/platform/internal/cliflag"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/instancefs"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// LiveDir is where the adoption pass keeps each host's live symlink; the
+// trampoline execs nothing that is not directly under it.
+const LiveDir = "/opt/carlos/live"
+
+var errSymlinkOrEscape = errors.New("instance-exec: launch file refused")
+
+// dataDir is the agent's own resolver (host/agent.go), repeated here because
+// this program runs in a namespace where only the instance's directory is
+// bound, and it must agree with the agent on where that directory is.
+func dataDir() string {
+ if d := os.Getenv("CARLOS_DATA_DIR"); d != "" {
+ return d
+ }
+ return "/data"
+}
+
+// RunExec is `carlos instance-exec <role> <iid>`.
+func RunExec(args []string) {
+ fs := flag.NewFlagSet("instance-exec", flag.ExitOnError)
+ fs.Usage = cliflag.Usage(fs, "usage: carlos instance-exec <run|seed|sidecar> <instance-id>")
+ _ = fs.Parse(args)
+ if fs.NArg() != 2 {
+ fs.Usage()
+ os.Exit(2)
+ }
+ role, ok := instancectl.ParseRole(fs.Arg(0))
+ if !ok {
+ log.Fatalf("instance-exec: role %q is not run, seed or sidecar", fs.Arg(0))
+ }
+ if err := execLaunch(dataDir(), role, fs.Arg(1), syscall.Exec); err != nil {
+ log.Fatalf("instance-exec: %v", err)
+ }
+}
+
+// execLaunch reads launch.<role>.json from the instance directory through
+// instancefs (never through a symlink), validates it, and execs. It returns
+// only on refusal; a successful exec never returns.
+//
+// The validation is defence in depth: the agent wrote the file, but the
+// directory is the instance's own, so the instance can rewrite it. What
+// the checks guarantee is that whatever is executed is directly under
+// LiveDir (the platform's live links, root-written) and named for the host
+// in the file, so a rewritten file can execute only what the tenant could
+// execute anyway: its own binary with an argv and env it chose.
+func execLaunch(dataDir string, role instancectl.Role, iid string, execFn func(argv0 string, argv, env []string) error) error {
+ dir := filepath.Join(dataDir, "instances", iid)
+ raw, err := instancefs.ReadBeneath(dir, instancectl.LaunchFileName(role))
+ if err != nil {
+ if errors.Is(err, instancefs.ErrSymlink) || errors.Is(err, instancefs.ErrEscapes) {
+ return fmt.Errorf("%w: %v", errSymlinkOrEscape, err)
+ }
+ return err
+ }
+ var l instancectl.Launch
+ if err := json.Unmarshal(raw, &l); err != nil {
+ return fmt.Errorf("launch file: %v", err)
+ }
+ if l.Schema != instancectl.LaunchSchema {
+ return fmt.Errorf("launch file schema %d is not %d", l.Schema, instancectl.LaunchSchema)
+ }
+ if !provision.ValidHostShape(l.Host) {
+ return fmt.Errorf("launch file host %q is not a host", l.Host)
+ }
+ if len(l.Argv) == 0 {
+ return errors.New("launch file has no argv")
+ }
+ argv0 := l.Argv[0]
+ if !filepath.IsAbs(argv0) || filepath.Clean(argv0) != argv0 || filepath.Dir(argv0) != LiveDir {
+ return fmt.Errorf("argv[0] %q is not directly under %s", argv0, LiveDir)
+ }
+ if filepath.Base(argv0) != l.Host {
+ return fmt.Errorf("argv[0] %q is not the live link of host %s", argv0, l.Host)
+ }
+ for _, kv := range l.Env {
+ if k, _, ok := strings.Cut(kv, "="); !ok || k == "" {
+ return fmt.Errorf("launch file env entry %q is not KEY=value", kv)
+ }
+ }
+ return execFn(argv0, l.Argv, l.Env)
+}
diff --git a/internal/instanceexec/exec_test.go b/internal/instanceexec/exec_test.go
new file mode 100644
index 000000000..454c97866
--- /dev/null
+++ b/internal/instanceexec/exec_test.go
@@ -0,0 +1,118 @@
+//go:build linux
+
+package instanceexec
+
+import (
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/instancectl"
+)
+
+func writeLaunch(t *testing.T, dir string, role instancectl.Role, l instancectl.Launch) {
+ t.Helper()
+ if err := os.MkdirAll(dir, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ b, err := json.Marshal(l)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, instancectl.LaunchFileName(role)), b, 0o600); err != nil {
+ t.Fatal(err)
+ }
+}
+
+type captured struct {
+ argv0 string
+ argv []string
+ env []string
+ calls int
+}
+
+func (c *captured) exec(argv0 string, argv, env []string) error {
+ c.calls++
+ c.argv0, c.argv, c.env = argv0, argv, env
+ return nil
+}
+
+func TestExecLaunchExecsExactlyWhatTheAgentWrote(t *testing.T) {
+ data := t.TempDir()
+ iid := "bill.example.com"
+ dir := filepath.Join(data, "instances", iid)
+ writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{
+ Schema: 1, Host: "bill.example.com",
+ Argv: []string{"/opt/carlos/live/bill.example.com", "--socket", "/run/carlos/instances/bill.example.com/app.sock", "--db", "/data/instances/bill.example.com/bill.example.com.db"},
+ Env: []string{"PATH=/usr/bin", "CARLOS_ADMIN_TOKEN=abc"},
+ })
+ var c captured
+ if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err != nil {
+ t.Fatalf("execLaunch: %v", err)
+ }
+ if c.calls != 1 || c.argv0 != "/opt/carlos/live/bill.example.com" || len(c.argv) != 5 || c.argv[2] != "/run/carlos/instances/bill.example.com/app.sock" {
+ t.Errorf("exec = %+v", c)
+ }
+ if len(c.env) != 2 || c.env[1] != "CARLOS_ADMIN_TOKEN=abc" {
+ t.Errorf("env = %v", c.env)
+ }
+}
+
+func TestExecLaunchRefusesAnArgv0OutsideTheLiveDir(t *testing.T) {
+ data := t.TempDir()
+ iid := "h.example.com"
+ dir := filepath.Join(data, "instances", iid)
+ for _, argv0 := range []string{"/bin/sh", "/opt/carlos/live/../carlos", "/opt/carlos/live/other.example.com", "opt/carlos/live/h.example.com", ""} {
+ writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: []string{argv0}, Env: nil})
+ var c captured
+ err := execLaunch(data, instancectl.RoleRun, iid, c.exec)
+ if err == nil || c.calls != 0 {
+ t.Errorf("argv0 %q: err = %v, calls = %d; want a refusal and no exec", argv0, err, c.calls)
+ }
+ }
+}
+
+func TestExecLaunchRefusesUnknownSchemaAndBadEnv(t *testing.T) {
+ data := t.TempDir()
+ iid := "h.example.com"
+ dir := filepath.Join(data, "instances", iid)
+ good := []string{"/opt/carlos/live/h.example.com"}
+ writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 2, Host: "h.example.com", Argv: good})
+ var c captured
+ if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
+ t.Errorf("schema 2: err = %v, calls = %d", err, c.calls)
+ }
+ writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: good, Env: []string{"NOEQUALS"}})
+ if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
+ t.Errorf("bad env: err = %v, calls = %d", err, c.calls)
+ }
+ writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "not a host", Argv: []string{"/opt/carlos/live/not a host"}})
+ if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
+ t.Errorf("bad host: err = %v, calls = %d", err, c.calls)
+ }
+}
+
+func TestExecLaunchRefusesASymlinkedLaunchFile(t *testing.T) {
+ data := t.TempDir()
+ iid := "h.example.com"
+ dir := filepath.Join(data, "instances", iid)
+ if err := os.MkdirAll(dir, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ other := filepath.Join(t.TempDir(), "launch.run.json")
+ l := instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: []string{"/opt/carlos/live/h.example.com"}}
+ b, _ := json.Marshal(l)
+ if err := os.WriteFile(other, b, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(other, filepath.Join(dir, "launch.run.json")); err != nil {
+ t.Fatal(err)
+ }
+ var c captured
+ err := execLaunch(data, instancectl.RoleRun, iid, c.exec)
+ if err == nil || c.calls != 0 || !errors.Is(err, errSymlinkOrEscape) {
+ t.Errorf("symlinked launch file: err = %v, calls = %d", err, c.calls)
+ }
+}