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 host
4
5import (
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/instancectl"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16const containedStatus = "Uid:\t65534\t65534\t65534\t65534\nCapInh:\t0000000000000000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\nCapBnd:\t0000000000000000\nCapAmb:\t0000000000000000\nNoNewPrivs:\t1\nSeccomp:\t2\n"
17
18func TestPlan14GenerationContainedTask(t *testing.T) {
19 for _, attack := range []string{"none", "closed history", "old record", "retired", "group identity", "main lifetime", "launcher", "namespace", "capability", "privilege gain", "missing filter", "unmasked", "other bind", "wrong app", "task group", "foreign proc", "foreign proc descriptor bind", "unrecognized mount root"} {
20 t.Run(attack, func(t *testing.T) {
21 f := generationProc(t)
22 group := instancectl.ClaimUnitControlGroup(f.reservation.Binding.Binding.Host)
23 cgroups, root := t.TempDir(), t.TempDir()
24 if err := os.MkdirAll(filepath.Join(cgroups, group), 0755); err != nil {
25 t.Fatal(err)
26 }
27 gid, err := generationIdentity(filepath.Join(cgroups, group))
28 if err != nil {
29 t.Fatal(err)
30 }
31 lid, err := generationIdentity(f.unrelated)
32 if err != nil {
33 t.Fatal(err)
34 }
35 r := f.reservation
36 r.Binding.Target.BootID = "boot-fixture"
37 r.Process = release.GenerationProcess{PID: 100, TenantPID: 200, Start: "123", TenantStart: "123", Group: group, Invocation: "invocation", GroupIdentity: gid, LauncherIdentity: lid, Containment: 1}
38 for _, pid := range []string{"100", "200"} {
39 p := filepath.Join(f.proc, pid)
40 if err := os.MkdirAll(p+"/ns", 0755); err != nil {
41 t.Fatal(err)
42 }
43 generationWrite(t, p+"/stat", pid+" (app) S 100 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 123 0\n")
44 generationWrite(t, p+"/cgroup", "0::/"+group+"\n")
45 for _, ns := range []string{"mnt", "pid"} {
46 generationLink(t, "confined-"+ns, p+"/ns/"+ns)
47 }
48 }
49 generationLink(t, f.unrelated, f.proc+"/100/exe")
50 generationLink(t, r.Binding.Path, f.proc+"/200/exe")
51 generationWrite(t, f.task+"/cgroup", "0::/"+group+"\n")
52 generationWrite(t, f.task+"/status", containedStatus)
53 generationRelink(t, root, f.task+"/root")
54 for _, ns := range []string{"mnt", "pid"} {
55 generationRelink(t, "confined-"+ns, f.task+"/ns/"+ns)
56 }
57 if err := os.MkdirAll(root+"/opt/carlos", 0755); err != nil {
58 t.Fatal(err)
59 }
60 generationLink(t, r.Binding.Path, root+"/opt/carlos/app")
61 generationWrite(t, f.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / ro - ext4 disk ro\n2 1 %d:%d %s /opt/carlos/app ro - ext4 disk ro\n", generationMajor(gid.Device), generationMinor(gid.Device), generationMajor(r.Binding.Executable.Device), generationMinor(r.Binding.Executable.Device), r.Binding.Path))
62 old := r
63 old.Closed = true
64 old.Binding.Attempt += "-old"
65 old.Process.Invocation = "old-invocation"
66 old.Process.GroupIdentity.Inode++
67 status := []byte(containedStatus)
68 switch attack {
69 case "old record":
70 r.Process.Containment = 0
71 case "retired":
72 r.Retirement = "retired"
73 case "group identity":
74 r.Process.GroupIdentity.Inode++
75 case "main lifetime":
76 r.Process.Start = "999"
77 case "launcher":
78 r.Process.LauncherIdentity.Inode++
79 case "namespace":
80 generationRelink(t, "namespace-mnt", f.task+"/ns/mnt")
81 case "capability":
82 status = []byte(strings.Replace(containedStatus, "CapEff:\t0000000000000000", "CapEff:\t0000000000000001", 1))
83 case "privilege gain":
84 status = []byte(strings.Replace(containedStatus, "NoNewPrivs:\t1", "NoNewPrivs:\t0", 1))
85 case "missing filter":
86 status = []byte(strings.Replace(containedStatus, "Seccomp:\t2", "Seccomp:\t0", 1))
87 case "unmasked":
88 if err := os.MkdirAll(filepath.Join(root, f.versions), 0755); err != nil {
89 t.Fatal(err)
90 }
91 case "other bind":
92 generationWrite(t, f.task+"/mountinfo", fmt.Sprintf("1 0 0:1 / / ro - tmpfs tmpfs ro\n2 1 %d:%d %s /alias ro - ext4 disk ro\n", generationMajor(r.Binding.Executable.Device), generationMinor(r.Binding.Executable.Device), filepath.Dir(r.Binding.Tree)))
93 case "unrecognized mount root":
94 generationWrite(t, f.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / ro - ext4 disk ro\n2 1 %d:%d /versions/v1/shop /opt/carlos/app ro - ext4 disk ro\n", generationMajor(gid.Device), generationMinor(gid.Device), generationMajor(r.Binding.Executable.Device), generationMinor(r.Binding.Executable.Device)))
95 case "wrong app":
96 generationRelink(t, f.unrelated, root+"/opt/carlos/app")
97 case "foreign proc", "foreign proc descriptor bind":
98 source := "/"
99 if attack == "foreign proc descriptor bind" {
100 source = "/777/fd"
101 }
102 generationWrite(t, f.task+"/mountinfo", "1 0 0:1 "+source+" /hostproc ro - proc proc ro\n")
103 case "task group":
104 generationWrite(t, f.task+"/cgroup", "0::/other\n")
105 }
106 k := generationKernel{proc: f.proc, cgroups: cgroups, contained: []release.GenerationReservation{r}, owner: uint32(os.Geteuid())}
107 if attack == "closed history" {
108 old := r
109 old.Closed = true
110 old.Process.Start = "dead"
111 k.contained = append([]release.GenerationReservation{old}, k.contained...)
112 }
113 ok, err := k.containedTask(t.Context(), f.task, status)
114 if attack == "none" || attack == "closed history" {
115 if !ok || err != nil {
116 t.Fatalf("contained tenant refused: %v %v", ok, err)
117 }
118 } else if ok && err == nil {
119 t.Fatal("unsafe containment accepted")
120 }
121 for _, pid := range []string{"100", "200"} {
122 p := filepath.Join(f.proc, pid)
123 generationWrite(t, p+"/status", string(status))
124 generationLink(t, root, p+"/root")
125 mounts, err := os.ReadFile(f.task + "/mountinfo")
126 if err != nil {
127 t.Fatal(err)
128 }
129 generationWrite(t, p+"/mountinfo", string(mounts))
130 }
131 for _, name := range []string{"status", "root", "mountinfo", "ns", "cgroup"} {
132 generationLink(t, f.task+"/"+name, f.proc+"/41/"+name)
133 }
134 generationWrite(t, filepath.Join(cgroups, group, "cgroup.procs"), "100\n200\n41\n")
135 err = k.replacedGroup(t.Context(), old)
136 if attack == "none" || attack == "closed history" {
137 if err != nil {
138 t.Fatal("independently contained replacement refused", err)
139 }
140 for _, mutation := range []string{"missing reservation", "live old", "same group", "same invocation", "other host", "unrecorded child"} {
141 t.Run(mutation, func(t *testing.T) {
142 candidate, prior, check := r, old, k
143 switch mutation {
144 case "live old":
145 prior.Closed = false
146 case "same group":
147 prior.Process.GroupIdentity = candidate.Process.GroupIdentity
148 case "same invocation":
149 prior.Process.Invocation = candidate.Process.Invocation
150 case "other host":
151 candidate.Binding.Binding.Host = "foreign.bab.example"
152 case "unrecorded child":
153 generationWrite(t, filepath.Join(cgroups, group, "cgroup.procs"), "100\n200\n999\n")
154 t.Cleanup(func() { generationWrite(t, filepath.Join(cgroups, group, "cgroup.procs"), "100\n200\n41\n") })
155 }
156 check.contained = []release.GenerationReservation{candidate}
157 if mutation == "missing reservation" {
158 check.contained = nil
159 }
160 if err := check.replacedGroup(t.Context(), prior); err == nil {
161 t.Fatal("unverified replacement accepted")
162 }
163 })
164 }
165 } else if err == nil {
166 t.Fatal("unsafe replacement accepted")
167 }
168 })
169 }
170}
171