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  }