carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package instanceexec
4
5import (
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "syscall"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/instancefs"
14 "github.com/carlosframework/platform/internal/provision"
15)
16
17// This is an opt-in kernel witness, not a mock or a go-test-on-the-host check.
18// The external sandbox wrapper must build this package's test binary and run
19// only this test through main's isolated tenant launcher as the tenant payload.
20// It must first verify that the edge's delegated cgroup controls exist and are
21// writable by carlos in the host namespace, and that all four journal paths
22// below are existing regular files in that namespace. Use disposable sandbox
23// fixtures, never create fixtures in a production journal directory. Set
24// CARLOS_CLAIM_ISOLATION_HOST to the sandbox host, then set both switches below
25// only after those host-side checks pass. The assertion cannot establish the
26// existence of files deliberately hidden from its own namespace.
27//
28// A missing opt-in skips. Missing host fixture attestation or incorrect mount
29// facts fail. Local CI skips do not prove isolation and cannot enable rollout.
30// Opens have no O_CREATE, O_TRUNC or write calls, even if isolation is broken.
31func TestClaimTenantCannotWriteContainmentOrJournals(t *testing.T) {
32 if os.Getenv("CARLOS_CLAIM_ISOLATION_KERNEL") != "1" {
33 t.Skip("requires sandbox fixtures and main's isolated tenant launcher")
34 }
35 if os.Getenv("CARLOS_CLAIM_ISOLATION_HOST_FIXTURES_VERIFIED") != "1" {
36 t.Fatal("external wrapper must verify host-side journal and delegated cgroup fixtures")
37 }
38 host := os.Getenv("CARLOS_CLAIM_ISOLATION_HOST")
39 if !provision.ValidHostShape(host) {
40 t.Fatal("sandbox host fixture is missing or invalid")
41 }
42 if os.Geteuid() == 0 || os.Getpid() != 1 {
43 t.Fatal("witness must run as the non-root tenant payload in its private PID namespace")
44 }
45 mounts, err := os.ReadFile("/proc/self/mountinfo")
46 if err != nil {
47 t.Fatal(err)
48 }
49 for mountpoint, filesystem := range map[string]string{"/data": "tmpfs", "/sys/fs/cgroup": "cgroup2"} {
50 readOnly, err := instancefs.MountOptionOf(mountpoint, "ro")
51 if err != nil || !readOnly {
52 t.Fatalf("%s must be a read-only mount: %v, %v", mountpoint, readOnly, err)
53 }
54 found := false
55 for _, line := range strings.Split(string(mounts), "\n") {
56 left, right, ok := strings.Cut(line, " - ")
57 fields, fs := strings.Fields(left), strings.Fields(right)
58 if ok && len(fields) >= 6 && fields[4] == mountpoint && len(fs) >= 1 {
59 found = fs[0] == filesystem
60 }
61 }
62 if !found {
63 t.Fatalf("%s must have filesystem %s in the tenant namespace", mountpoint, filesystem)
64 }
65 }
66 // The tenant's /data is a fresh read-only tmpfs exposing only instances.
67 // This is the hiding mechanism; ENOENT alone would be a vacuous assertion.
68 entries, err := os.ReadDir("/data")
69 if err != nil || len(entries) != 1 || entries[0].Name() != "instances" || !entries[0].IsDir() {
70 t.Fatalf("unexpected tenant data namespace: %+v, %v", entries, err)
71 }
72 for _, name := range []string{"cgroup.procs", "cgroup.threads", "cgroup.subtree_control"} {
73 path := filepath.Join("/sys/fs/cgroup/carlos.slice/carlos-edge.service", name)
74 info, err := os.Stat(path)
75 if err != nil || !info.Mode().IsRegular() {
76 t.Fatalf("delegated control fixture %s must exist: %v", path, err)
77 }
78 stat, ok := info.Sys().(*syscall.Stat_t)
79 if !ok || int(stat.Uid) != os.Geteuid() || info.Mode().Perm()&0200 == 0 {
80 t.Fatalf("%s must be delegated to the tenant UID before read-only mount enforcement", path)
81 }
82 f, err := os.OpenFile(path, os.O_WRONLY, 0)
83 if err == nil {
84 f.Close()
85 t.Fatalf("tenant can open %s for write", path)
86 }
87 if !errors.Is(err, syscall.EROFS) {
88 t.Fatalf("%s refusal must be EROFS, not missing fixture or DAC: %v", path, err)
89 }
90 }
91 // Opening cgroup.procs and cgroup.threads for write is necessary to move
92 // a process or thread. Their refusal proves migration is unavailable
93 // without attempting to move this process or alter another cgroup.
94 for _, path := range []string{
95 "/data/.hostclaims/launches.db",
96 "/data/.hostclaims/inventory.db",
97 "/data/.hostguard/claim-isolation-fixture.json",
98 filepath.Join("/data/hostguard", host+".json"),
99 } {
100 f, err := os.OpenFile(path, os.O_WRONLY, 0)
101 if err == nil {
102 f.Close()
103 t.Fatalf("tenant can open journal %s for write", path)
104 }
105 if !errors.Is(err, syscall.ENOENT) {
106 t.Fatalf("journal %s must be hidden by the verified tenant data mount: %v", path, err)
107 }
108 }
109}
110