gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/container/trace_test.go (about) 1 // Copyright 2022 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package container 16 17 import ( 18 "encoding/json" 19 "io/ioutil" 20 "os" 21 "strings" 22 "testing" 23 "time" 24 25 specs "github.com/opencontainers/runtime-spec/specs-go" 26 "golang.org/x/sys/unix" 27 "google.golang.org/protobuf/proto" 28 "gvisor.dev/gvisor/pkg/sentry/kernel" 29 "gvisor.dev/gvisor/pkg/sentry/limits" 30 "gvisor.dev/gvisor/pkg/sentry/seccheck" 31 pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" 32 "gvisor.dev/gvisor/pkg/sentry/seccheck/sinks/remote/test" 33 "gvisor.dev/gvisor/pkg/test/testutil" 34 "gvisor.dev/gvisor/runsc/boot" 35 ) 36 37 func remoteSinkConfig(endpoint string) seccheck.SinkConfig { 38 return seccheck.SinkConfig{ 39 Name: "remote", 40 Config: map[string]any{ 41 "endpoint": endpoint, 42 }, 43 } 44 } 45 46 // Test that setting up a trace session configuration in PodInitConfig creates 47 // a session before container creation. 48 func TestTraceStartup(t *testing.T) { 49 // Test on all configurations to ensure that point can be sent to an outside 50 // process in all cases. Rest of the tests don't require all configs. 51 for name, conf := range configs(t, false /* noOverlay */) { 52 t.Run(name, func(t *testing.T) { 53 server, err := test.NewServer() 54 if err != nil { 55 t.Fatalf("newServer(): %v", err) 56 } 57 defer server.Close() 58 59 podInitConfig, err := ioutil.TempFile(testutil.TmpDir(), "config") 60 if err != nil { 61 t.Fatalf("error creating tmp file: %v", err) 62 } 63 defer podInitConfig.Close() 64 65 initConfig := boot.InitConfig{ 66 TraceSession: seccheck.SessionConfig{ 67 Name: seccheck.DefaultSessionName, 68 Points: []seccheck.PointConfig{ 69 { 70 Name: "container/start", 71 ContextFields: []string{"container_id"}, 72 }, 73 }, 74 Sinks: []seccheck.SinkConfig{remoteSinkConfig(server.Endpoint)}, 75 }, 76 } 77 encoder := json.NewEncoder(podInitConfig) 78 if err := encoder.Encode(&initConfig); err != nil { 79 t.Fatalf("JSON encode: %v", err) 80 } 81 conf.PodInitConfig = podInitConfig.Name() 82 83 spec := testutil.NewSpecWithArgs("/bin/true") 84 if err := run(spec, conf); err != nil { 85 t.Fatalf("Error running container: %v", err) 86 } 87 88 // Wait for the point to be received and then check that fields match. 89 server.WaitForCount(1) 90 pt := server.GetPoints()[0] 91 if want := pb.MessageType_MESSAGE_CONTAINER_START; pt.MsgType != want { 92 t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) 93 } 94 got := &pb.Start{} 95 if err := proto.Unmarshal(pt.Msg, got); err != nil { 96 t.Errorf("proto.Unmarshal(Start): %v", err) 97 } 98 if want := "/bin/true"; len(got.Args) != 1 || want != got.Args[0] { 99 t.Errorf("container.Start.Args, want: %q, got: %q", want, got.Args) 100 } 101 if want, got := got.Id, got.ContextData.ContainerId; want != got { 102 t.Errorf("Mismatched container ID, want: %v, got: %v", want, got) 103 } 104 }) 105 } 106 } 107 108 func TestTraceLifecycle(t *testing.T) { 109 spec, conf := sleepSpecConf(t) 110 _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) 111 if err != nil { 112 t.Fatalf("error setting up container: %v", err) 113 } 114 defer cleanup() 115 116 // Create and start the container. 117 args := Args{ 118 ID: testutil.RandomContainerID(), 119 Spec: spec, 120 BundleDir: bundleDir, 121 } 122 cont, err := New(conf, args) 123 if err != nil { 124 t.Fatalf("error creating container: %v", err) 125 } 126 defer cont.Destroy() 127 if err := cont.Start(conf); err != nil { 128 t.Fatalf("error starting container: %v", err) 129 } 130 131 // Check that no session are created. 132 if sessions, err := cont.Sandbox.ListTraceSessions(); err != nil { 133 t.Fatalf("ListTraceSessions(): %v", err) 134 } else if len(sessions) != 0 { 135 t.Fatalf("no session should exist, got: %+v", sessions) 136 } 137 138 // Create a new trace session on the fly. 139 server, err := test.NewServer() 140 if err != nil { 141 t.Fatalf("newServer(): %v", err) 142 } 143 defer server.Close() 144 145 session := seccheck.SessionConfig{ 146 Name: "Default", 147 Points: []seccheck.PointConfig{ 148 { 149 Name: "sentry/task_exit", 150 ContextFields: []string{"container_id"}, 151 }, 152 }, 153 Sinks: []seccheck.SinkConfig{remoteSinkConfig(server.Endpoint)}, 154 } 155 if err := cont.Sandbox.CreateTraceSession(&session, false); err != nil { 156 t.Fatalf("CreateTraceSession(): %v", err) 157 } 158 159 // Trigger the configured point and want to receive it in the server. 160 if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { 161 t.Fatalf("exec: true, ws: %v, err: %v", ws, err) 162 } 163 server.WaitForCount(1) 164 pt := server.GetPoints()[0] 165 if want := pb.MessageType_MESSAGE_SENTRY_TASK_EXIT; pt.MsgType != want { 166 t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) 167 } 168 got := &pb.TaskExit{} 169 if err := proto.Unmarshal(pt.Msg, got); err != nil { 170 t.Errorf("proto.Unmarshal(TaskExit): %v", err) 171 } 172 if got.ExitStatus != 0 { 173 t.Errorf("Wrong TaskExit.ExitStatus, want: 0, got: %+v", got) 174 } 175 if want, got := cont.ID, got.ContextData.ContainerId; want != got { 176 t.Errorf("Wrong TaskExit.ContextData.ContainerId, want: %v, got: %v", want, got) 177 } 178 179 // Check that no more points were received and reset the server for the 180 // remaining tests. 181 if want, got := 1, server.Reset(); want != got { 182 t.Errorf("wrong number of points, want: %d, got: %d", want, got) 183 } 184 185 // List and check that trace session is reported. 186 sessions, err := cont.Sandbox.ListTraceSessions() 187 if err != nil { 188 t.Fatalf("ListTraceSessions(): %v", err) 189 } 190 if len(sessions) != 1 { 191 t.Fatalf("expected a single session, got: %+v", sessions) 192 } 193 if got := sessions[0].Name; seccheck.DefaultSessionName != got { 194 t.Errorf("wrong session, want: %v, got: %v", seccheck.DefaultSessionName, got) 195 } 196 197 if err := cont.Sandbox.DeleteTraceSession("Default"); err != nil { 198 t.Fatalf("DeleteTraceSession(): %v", err) 199 } 200 201 // Check that session was indeed deleted. 202 if sessions, err := cont.Sandbox.ListTraceSessions(); err != nil { 203 t.Fatalf("ListTraceSessions(): %v", err) 204 } else if len(sessions) != 0 { 205 t.Fatalf("no session should exist, got: %+v", sessions) 206 } 207 208 // Trigger the point again and check that it's not received. 209 if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { 210 t.Fatalf("exec: true, ws: %v, err: %v", ws, err) 211 } 212 time.Sleep(time.Second) // give some time to receive the point. 213 if server.Count() > 0 { 214 t.Errorf("point received after session was deleted: %+v", server.GetPoints()) 215 } 216 } 217 218 func TestTraceForceCreate(t *testing.T) { 219 spec, conf := sleepSpecConf(t) 220 _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) 221 if err != nil { 222 t.Fatalf("error setting up container: %v", err) 223 } 224 defer cleanup() 225 226 // Create and start the container. 227 args := Args{ 228 ID: testutil.RandomContainerID(), 229 Spec: spec, 230 BundleDir: bundleDir, 231 } 232 cont, err := New(conf, args) 233 if err != nil { 234 t.Fatalf("error creating container: %v", err) 235 } 236 defer cont.Destroy() 237 if err := cont.Start(conf); err != nil { 238 t.Fatalf("error starting container: %v", err) 239 } 240 241 // Create a new trace session that will be overwritten. 242 server, err := test.NewServer() 243 if err != nil { 244 t.Fatalf("newServer(): %v", err) 245 } 246 defer server.Close() 247 248 session := seccheck.SessionConfig{ 249 Name: "Default", 250 Points: []seccheck.PointConfig{ 251 {Name: "sentry/exit_notify_parent"}, 252 }, 253 Sinks: []seccheck.SinkConfig{remoteSinkConfig(server.Endpoint)}, 254 } 255 if err := cont.Sandbox.CreateTraceSession(&session, false); err != nil { 256 t.Fatalf("CreateTraceSession(): %v", err) 257 } 258 259 // Trigger the configured point to check that trace session is enabled. 260 if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { 261 t.Fatalf("exec: true, ws: %v, err: %v", ws, err) 262 } 263 server.WaitForCount(1) 264 pt := server.GetPoints()[0] 265 if want := pb.MessageType_MESSAGE_SENTRY_EXIT_NOTIFY_PARENT; pt.MsgType != want { 266 t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) 267 } 268 server.Reset() 269 270 // Check that creating the same session fails. 271 if err := cont.Sandbox.CreateTraceSession(&session, false); err == nil || !strings.Contains(err.Error(), "already exists") { 272 t.Errorf("CreateTraceSession() again failed with wrong error: %v", err) 273 } 274 275 // Re-create the session with a different point using force=true and check 276 // that it overwrote the other trace session. 277 session = seccheck.SessionConfig{ 278 Name: "Default", 279 Points: []seccheck.PointConfig{ 280 {Name: "sentry/task_exit"}, 281 }, 282 Sinks: []seccheck.SinkConfig{remoteSinkConfig(server.Endpoint)}, 283 } 284 if err := cont.Sandbox.CreateTraceSession(&session, true); err != nil { 285 t.Fatalf("CreateTraceSession(force): %v", err) 286 } 287 288 if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { 289 t.Fatalf("exec: true, ws: %v, err: %v", ws, err) 290 } 291 server.WaitForCount(1) 292 pt = server.GetPoints()[0] 293 if want := pb.MessageType_MESSAGE_SENTRY_TASK_EXIT; pt.MsgType != want { 294 t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) 295 } 296 } 297 298 func TestProcfsDump(t *testing.T) { 299 spec, conf := sleepSpecConf(t) 300 testEnv := "GVISOR_IS_GREAT=true" 301 spec.Process.Env = append(spec.Process.Env, testEnv) 302 spec.Process.Cwd = "/" 303 fdLimit := limits.Limit{ 304 Cur: 10_000, 305 Max: 100_000, 306 } 307 spec.Process.Rlimits = []specs.POSIXRlimit{ 308 {Type: "RLIMIT_NOFILE", Hard: fdLimit.Max, Soft: fdLimit.Cur}, 309 } 310 _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) 311 if err != nil { 312 t.Fatalf("error setting up container: %v", err) 313 } 314 defer cleanup() 315 316 // Create and start the container. 317 args := Args{ 318 ID: testutil.RandomContainerID(), 319 Spec: spec, 320 BundleDir: bundleDir, 321 } 322 cont, err := New(conf, args) 323 if err != nil { 324 t.Fatalf("error creating container: %v", err) 325 } 326 defer cont.Destroy() 327 if err := cont.Start(conf); err != nil { 328 t.Fatalf("error starting container: %v", err) 329 } 330 331 startTime := time.Now().UnixNano() 332 procfsDump, err := cont.Sandbox.ProcfsDump() 333 if err != nil { 334 t.Fatalf("ProcfsDump() failed: %v", err) 335 } 336 337 // Sleep should be the only process running in the container. 338 if len(procfsDump) != 1 { 339 t.Fatalf("got incorrect number of proc results: %+v", procfsDump) 340 } 341 342 // Sleep should be PID 1. 343 if procfsDump[0].Status.PID != 1 { 344 t.Errorf("expected sleep process to be pid 1, got %d", procfsDump[0].Status.PID) 345 } 346 347 // Check that bin/sleep is part of the executable path. 348 if wantExeSubStr := "bin/sleep"; !strings.HasSuffix(procfsDump[0].Exe, wantExeSubStr) { 349 t.Errorf("expected %q to be part of execuable path %q", wantExeSubStr, procfsDump[0].Exe) 350 } 351 352 if len(procfsDump[0].Args) != 2 { 353 t.Errorf("expected 2 args, but got %+v", procfsDump[0].Args) 354 } else { 355 if procfsDump[0].Args[0] != "sleep" || procfsDump[0].Args[1] != "1000" { 356 t.Errorf("expected args %q but got %+v", "sleep 1000", procfsDump[0].Args) 357 } 358 } 359 360 testEnvFound := false 361 for _, env := range procfsDump[0].Env { 362 if env == testEnv { 363 testEnvFound = true 364 } 365 } 366 if !testEnvFound { 367 t.Errorf("expected to find %q env but did not find it, got env %+v", testEnv, procfsDump[0].Env) 368 } 369 370 if spec.Process.Cwd != procfsDump[0].CWD { 371 t.Errorf("expected CWD %q, got %q", spec.Process.Cwd, procfsDump[0].CWD) 372 } 373 374 // Expect at least 3 host FDs for stdout, stdin and stderr. 375 if len(procfsDump[0].FDs) < 3 { 376 t.Errorf("expected at least 3 FDs for the sleep process, got %+v", procfsDump[0].FDs) 377 } else { 378 modes := [3]uint32{} 379 for i, _ := range []*os.File{os.Stdin, os.Stdout, os.Stderr} { 380 stat := unix.Stat_t{} 381 err := unix.Fstat(i, &stat) 382 if err != nil { 383 t.Fatalf("unix.Fatat(i) failed: %s", err) 384 } 385 modes[i] = stat.Mode & unix.S_IFMT 386 } 387 for i, fd := range procfsDump[0].FDs[:3] { 388 if want := int32(i); fd.Number != want { 389 t.Errorf("expected FD number %d, got %d", want, fd.Number) 390 } 391 if wantSubStr := "host"; !strings.Contains(fd.Path, wantSubStr) { 392 t.Errorf("expected FD %d path to contain %q, got %q", fd.Number, wantSubStr, fd.Path) 393 } 394 if want, got := modes[i], fd.Mode&unix.S_IFMT; uint16(want) != got { 395 t.Errorf("wrong mode FD %d, want: %#o, got: %#o", fd.Number, want, got) 396 } 397 } 398 } 399 400 // Start time should be at most 3 second away from our locally calculated 401 // start time. Local startTime was calculated after container started, so 402 // process start time must be earlier than local startTime. 403 if startTime-procfsDump[0].StartTime > 3*time.Second.Nanoseconds() { 404 t.Errorf("wanted start time to be around %s, but got %s", time.Unix(0, startTime), time.Unix(0, procfsDump[0].StartTime)) 405 } 406 407 if want := "/"; procfsDump[0].Root != "/" { 408 t.Errorf("expected root to be %q, but got %q", want, procfsDump[0].Root) 409 } 410 411 if got := procfsDump[0].Limits["RLIMIT_NOFILE"]; got != fdLimit { 412 t.Errorf("expected FD limit to be %+v, but got %+v", fdLimit, got) 413 } 414 415 wantCgroup := []kernel.TaskCgroupEntry{ 416 kernel.TaskCgroupEntry{HierarchyID: 7, Controllers: "pids", Path: "/"}, 417 kernel.TaskCgroupEntry{HierarchyID: 6, Controllers: "memory", Path: "/"}, 418 kernel.TaskCgroupEntry{HierarchyID: 5, Controllers: "job", Path: "/"}, 419 kernel.TaskCgroupEntry{HierarchyID: 4, Controllers: "devices", Path: "/"}, 420 kernel.TaskCgroupEntry{HierarchyID: 3, Controllers: "cpuset", Path: "/"}, 421 kernel.TaskCgroupEntry{HierarchyID: 2, Controllers: "cpuacct", Path: "/"}, 422 kernel.TaskCgroupEntry{HierarchyID: 1, Controllers: "cpu", Path: "/"}, 423 } 424 if len(procfsDump[0].Cgroup) != len(wantCgroup) { 425 t.Errorf("expected 7 cgroup controllers, got %+v", procfsDump[0].Cgroup) 426 } else { 427 for i, cgroup := range procfsDump[0].Cgroup { 428 if cgroup != wantCgroup[i] { 429 t.Errorf("expected %+v, got %+v", wantCgroup[i], cgroup) 430 } 431 } 432 } 433 434 if wantPPID := int32(0); procfsDump[0].Status.PPID != wantPPID { 435 t.Errorf("expected PPID to be %d, but got %d", wantPPID, procfsDump[0].Status.PPID) 436 } 437 438 if wantName := "sleep"; procfsDump[0].Status.Comm != wantName { 439 t.Errorf("expected Comm to be %q, but got %q", wantName, procfsDump[0].Status.Comm) 440 } 441 442 if uid := procfsDump[0].Status.UID; uid.Real != 0 || uid.Effective != 0 || uid.Saved != 0 { 443 t.Errorf("expected UIDs to be 0 (root), got %+v", uid) 444 } 445 if gid := procfsDump[0].Status.GID; gid.Real != 0 || gid.Effective != 0 || gid.Saved != 0 { 446 t.Errorf("expected GIDs to be 0 (root), got %+v", gid) 447 } 448 449 if procfsDump[0].Status.VMSize == 0 { 450 t.Errorf("expected VMSize to be set") 451 } 452 if procfsDump[0].Status.VMRSS == 0 { 453 t.Errorf("expected VMSize to be set") 454 } 455 if len(procfsDump[0].Maps) <= 0 { 456 t.Errorf("no region mapped for pid:%v", procfsDump[0].Status.PID) 457 } 458 459 maps := procfsDump[0].Maps 460 for i := 0; i < len(procfsDump[0].Maps)-1; i++ { 461 if maps[i].Address.Overlaps(maps[i+1].Address) { 462 t.Errorf("overlapped addresses for pid:%v", procfsDump[0].Status.PID) 463 } 464 } 465 }