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 "bytes"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "os/user"
12 "path/filepath"
13 "runtime"
14 "strconv"
15 "strings"
16 "syscall"
17 "testing"
18
19 "github.com/carlosframework/platform/internal/registry"
20 "golang.org/x/sys/unix"
21)
22
23func TestRegistryChildHardeningHelper(t *testing.T) {
24 if os.Args[len(os.Args)-1] != "--hardening-probe" {
25 return
26 }
27 if os.Geteuid() != 0 {
28 t.Fatal("child lost root before its in-process credential drop")
29 }
30 service, err := user.Lookup(serviceUser)
31 if err != nil {
32 t.Fatal(err)
33 }
34 uid, err := strconv.Atoi(service.Uid)
35 if err != nil {
36 t.Fatal(err)
37 }
38 gid, err := strconv.Atoi(service.Gid)
39 if err != nil {
40 t.Fatal(err)
41 }
42 // Malformed input must still drop all credentials and disable dumpability.
43 if err := RunRegistryChild(nil); err == nil {
44 t.Fatal("invalid arguments accepted")
45 }
46 ruid, euid, suid := unix.Getresuid()
47 rgid, egid, sgid := unix.Getresgid()
48 groups, err := os.Getgroups()
49 if err != nil || ruid != uid || euid != uid || suid != uid || rgid != gid || egid != gid || sgid != gid || len(groups) != 0 {
50 t.Fatalf("child credentials = uid %d:%d:%d, gid %d:%d:%d, groups %v: %v", ruid, euid, suid, rgid, egid, sgid, groups, err)
51 }
52 dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0)
53 if err != nil || dumpable != 0 {
54 t.Fatalf("dumpable = %d, %v", dumpable, err)
55 }
56 threads, err := os.ReadDir("/proc/self/task")
57 if err != nil {
58 t.Fatal(err)
59 }
60 for _, thread := range threads {
61 status, err := os.ReadFile(filepath.Join("/proc/self/task", thread.Name(), "status"))
62 if os.IsNotExist(err) {
63 continue
64 }
65 if err != nil || !bytes.Contains(status, []byte("NoNewPrivs:\t1\n")) {
66 t.Fatalf("thread %s lacks no-new-privileges: %v", thread.Name(), err)
67 }
68 for _, want := range []string{
69 fmt.Sprintf("Uid:\t%d\t%d\t%d\t%d\n", uid, uid, uid, uid),
70 fmt.Sprintf("Gid:\t%d\t%d\t%d\t%d\n", gid, gid, gid, gid),
71 "Groups:\t\n",
72 } {
73 if !bytes.Contains(status, []byte(want)) {
74 t.Fatalf("thread %s lacks exact credentials %q: %s", thread.Name(), want, status)
75 }
76 }
77 }
78 if err := syscall.Setresuid(0, 0, 0); err == nil {
79 t.Fatal("child regained root uid")
80 }
81 if err := syscall.Setresgid(0, 0, 0); err == nil {
82 t.Fatal("child regained root gid")
83 }
84}
85
86func TestRegistryChildHardening(t *testing.T) {
87 if os.Geteuid() != 0 {
88 t.Skip("requires root to verify the child's in-process credential drop and dumpability")
89 }
90 if _, err := user.Lookup(serviceUser); err != nil {
91 t.Skip("requires the carlos service account")
92 }
93 // Keep this thread pinned while verifying the spawn did not change it.
94 runtime.LockOSThread()
95 defer runtime.UnlockOSThread()
96 before, err := unix.PrctlRetInt(unix.PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0)
97 if err != nil {
98 t.Fatal(err)
99 }
100 executable, err := os.Executable()
101 if err != nil {
102 t.Fatal(err)
103 }
104 cmd := exec.Command(executable, "-test.run=^TestRegistryChildHardeningHelper$", "--", "--hardening-probe")
105 var output bytes.Buffer
106 cmd.Stdout, cmd.Stderr = &output, &output
107 if err := registryChildStart(cmd); err != nil {
108 t.Fatal(err)
109 }
110 if err := cmd.Wait(); err != nil {
111 t.Fatalf("hardening probe: %v: %s", err, output.String())
112 }
113 after, err := unix.PrctlRetInt(unix.PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0)
114 if err != nil || after != before {
115 t.Fatalf("parent no-new-privileges changed from %d to %d: %v", before, after, err)
116 }
117}
118
119func TestRegistryChildFDHelper(t *testing.T) {
120 if len(os.Args) == 0 || os.Args[len(os.Args)-1] != "--fd-probe" {
121 return
122 }
123 closeRegistryExecutable()
124 entries, err := os.ReadDir("/proc/self/fd")
125 if err != nil {
126 os.Exit(20)
127 }
128 for _, entry := range entries {
129 fd, err := strconv.Atoi(entry.Name())
130 if err != nil || fd <= 2 {
131 continue
132 }
133 target, err := os.Readlink("/proc/self/fd/" + entry.Name())
134 if os.IsNotExist(err) {
135 continue
136 }
137 if err != nil {
138 os.Exit(21)
139 }
140 if target != "anon_inode:[eventpoll]" && target != "anon_inode:[eventfd]" {
141 os.Exit(22)
142 }
143 }
144 os.Exit(0)
145}
146
147func TestRegistryChildPinnedExecutableAndMinimalFDs(t *testing.T) {
148 dir := t.TempDir()
149 original, err := os.Open("/proc/self/exe")
150 if err != nil {
151 t.Fatal(err)
152 }
153 defer original.Close()
154 path := filepath.Join(dir, "worker")
155 destination, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755)
156 if err != nil {
157 t.Fatal(err)
158 }
159 if _, err := io.Copy(destination, original); err != nil {
160 t.Fatal(err)
161 }
162 if err := destination.Close(); err != nil {
163 t.Fatal(err)
164 }
165 pinned, err := os.Open(path)
166 if err != nil {
167 t.Fatal(err)
168 }
169 defer pinned.Close()
170 saved, savedErr := registryExecutable, registryExecutableError
171 registryExecutable, registryExecutableError = pinned, nil
172 defer func() { registryExecutable, registryExecutableError = saved, savedErr }()
173 if err := os.Rename(path, path+".old"); err != nil {
174 t.Fatal(err)
175 }
176 if err := os.WriteFile(path, []byte("replacement is not executable"), 0755); err != nil {
177 t.Fatal(err)
178 }
179 inherited, err := os.OpenFile(filepath.Join(dir, "must-not-inherit"), os.O_CREATE|os.O_RDWR, 0600)
180 if err != nil {
181 t.Fatal(err)
182 }
183 defer inherited.Close()
184 if _, err := unix.FcntlInt(inherited.Fd(), unix.F_SETFD, 0); err != nil {
185 t.Fatal(err)
186 }
187 cmd, err := registryChildExec("unused", 65534, 65534)
188 if err != nil {
189 t.Fatal(err)
190 }
191 cmd.Args = []string{cmd.Path, "-test.run=^TestRegistryChildFDHelper$", "--", "--fd-probe"}
192 if err := registryChildCloseExec(); err != nil {
193 t.Fatal(err)
194 }
195 if output, err := cmd.CombinedOutput(); err != nil {
196 t.Fatalf("pinned child/fd probe: %v: %s", err, output)
197 }
198}
199
200func TestRegistryChildRealMainStartup(t *testing.T) {
201 if os.Geteuid() != 0 {
202 t.Skip("requires root to exercise the real service boundary")
203 }
204 service, err := user.Lookup(serviceUser)
205 if err != nil {
206 t.Skip("requires the carlos service account")
207 }
208 uid, err := strconv.Atoi(service.Uid)
209 if err != nil || uid <= 0 {
210 t.Fatal("invalid service uid")
211 }
212 gid, err := strconv.Atoi(service.Gid)
213 if err != nil {
214 t.Fatal(err)
215 }
216 dir, err := os.MkdirTemp(os.TempDir(), "registry-real-main-")
217 if err != nil {
218 t.Fatal(err)
219 }
220 defer os.RemoveAll(dir)
221 if err := os.Chmod(dir, 0755); err != nil {
222 t.Fatal(err)
223 }
224 path := filepath.Join(dir, "carlos")
225 build := exec.Command("go", "build", "-o", path, ".")
226 build.Dir = "../.."
227 if output, err := build.CombinedOutput(); err != nil {
228 t.Fatalf("build real main: %v: %s", err, output)
229 }
230 pinned, err := os.Open(path)
231 if err != nil {
232 t.Fatal(err)
233 }
234 defer pinned.Close()
235 saved, savedErr := registryExecutable, registryExecutableError
236 registryExecutable, registryExecutableError = pinned, nil
237 defer func() { registryExecutable, registryExecutableError = saved, savedErr }()
238 if err := os.Rename(path, path+".old"); err != nil {
239 t.Fatal(err)
240 }
241 if err := os.WriteFile(path, []byte("replacement"), 0755); err != nil {
242 t.Fatal(err)
243 }
244 data := filepath.Join(dir, "data")
245 if err := os.Mkdir(data, 0700); err != nil {
246 t.Fatal(err)
247 }
248 if err := os.Chown(data, uid, gid); err != nil {
249 t.Fatal(err)
250 }
251 dbPath := filepath.Join(data, "registry.db")
252 t.Setenv("CARLOS_HOST_CLAIM", "true")
253 r, err := openServiceRegistry(dbPath)
254 if err != nil {
255 t.Fatal(err)
256 }
257 defer r.Close()
258 if err := r.Upsert(registry.Route{Host: "real-main", Kind: "instance"}); err != nil {
259 t.Fatal(err)
260 }
261 for _, suffix := range []string{"", "-wal", "-shm"} {
262 info, err := os.Stat(dbPath + suffix)
263 if err != nil {
264 t.Fatal(err)
265 }
266 gotUID, gotGID, ok := fileOwner(info)
267 if !ok || gotUID != uid || gotGID != gid {
268 t.Fatalf("%s owner = %d:%d", strings.TrimPrefix(dbPath+suffix, dir), gotUID, gotGID)
269 }
270 }
271}
272