diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index 40b6439cd..910bda6da 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -121,6 +121,7 @@ go_library( "//runsc/boot/filter", "//runsc/boot/platforms", "//runsc/boot/pprof", + "//runsc/boot/procfs", "//runsc/config", "//runsc/specutils", "//runsc/specutils/seccomp", diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index f4bb4c601..c87876187 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -38,6 +38,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/urpc" "gvisor.dev/gvisor/runsc/boot/pprof" + "gvisor.dev/gvisor/runsc/boot/procfs" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/specutils" ) @@ -90,6 +91,9 @@ const ( // ContMgrListTraceSessions lists a trace session. ContMgrListTraceSessions = "containerManager.ListTraceSessions" + + // ContMgrProcfsDump dumps sandbox procfs state. + ContMgrProcfsDump = "containerManager.ProcfsDump" ) const ( @@ -635,3 +639,21 @@ func (cm *containerManager) ListTraceSessions(_ *struct{}, out *[]seccheck.Sessi seccheck.List(out) return nil } + +// ProcfsDump dumps procfs state of the sandbox. +func (cm *containerManager) ProcfsDump(_ *struct{}, out *[]procfs.ProcessProcfsDump) error { + log.Debugf("containerManager.ProcfsDump") + ts := cm.l.k.TaskSet() + pidns := ts.Root + *out = make([]procfs.ProcessProcfsDump, 0, len(cm.l.processes)) + for _, tg := range pidns.ThreadGroups() { + pid := pidns.IDOfThreadGroup(tg) + procDump, err := procfs.Dump(tg.Leader(), pid) + if err != nil { + log.Warningf("skipping procfs dump for PID %s: %v", pid, err) + continue + } + *out = append(*out, procDump) + } + return nil +} diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 28cc346dc..e1a5751ff 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -139,7 +139,7 @@ type Loader struct { // processes are keyed with container ID and pid=0, while exec invocations // have the corresponding pid set. // - // processes is guardded by mu. + // processes is guarded by mu. processes map[execID]*execProcess // mountHints provides extra information about mounts for containers that diff --git a/runsc/boot/procfs/BUILD b/runsc/boot/procfs/BUILD new file mode 100644 index 000000000..1221423d7 --- /dev/null +++ b/runsc/boot/procfs/BUILD @@ -0,0 +1,15 @@ +load("//tools:defs.bzl", "go_library") + +package(licenses = ["notice"]) + +go_library( + name = "procfs", + srcs = ["dump.go"], + visibility = ["//runsc:__subpackages__"], + deps = [ + "//pkg/context", + "//pkg/log", + "//pkg/sentry/kernel", + "//pkg/sentry/mm", + ], +) diff --git a/runsc/boot/procfs/dump.go b/runsc/boot/procfs/dump.go new file mode 100644 index 000000000..d39bc743f --- /dev/null +++ b/runsc/boot/procfs/dump.go @@ -0,0 +1,75 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package procfs holds utilities for getting procfs information for sandboxed +// processes. +package procfs + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/mm" +) + +// ProcessProcfsDump contains the procfs dump for one process. +type ProcessProcfsDump struct { + // PID is the process ID. + PID int32 `json:"pid,omitempty"` + // Exe is the symlink target of /proc/[pid]/exe. + Exe string `json:"exe,omitempty"` +} + +// getMM returns t's MemoryManager. On success, the MemoryManager's users count +// is incremented, and must be decremented by the caller when it is no longer +// in use. +func getMM(t *kernel.Task) *mm.MemoryManager { + var mm *mm.MemoryManager + t.WithMuLocked(func(*kernel.Task) { + mm = t.MemoryManager() + }) + if mm == nil || !mm.IncUsers() { + return nil + } + return mm +} + +func getExecutablePath(ctx context.Context, pid kernel.ThreadID, mm *mm.MemoryManager) string { + exec := mm.Executable() + if exec == nil { + log.Warningf("No executable found for PID %s", pid) + return "" + } + defer exec.DecRef(ctx) + + return exec.PathnameWithDeleted(ctx) +} + +// Dump returns a procfs dump for process pid. t must be a task in process pid. +func Dump(t *kernel.Task, pid kernel.ThreadID) (ProcessProcfsDump, error) { + ctx := t.AsyncContext() + + mm := getMM(t) + if mm == nil { + return ProcessProcfsDump{}, fmt.Errorf("no MM found for PID %s", pid) + } + defer mm.DecUsers(ctx) + + return ProcessProcfsDump{ + PID: int32(pid), + Exe: getExecutablePath(ctx, pid, mm), + }, nil +} diff --git a/runsc/cmd/trace/BUILD b/runsc/cmd/trace/BUILD index 835cb0dd5..ed67f8aad 100644 --- a/runsc/cmd/trace/BUILD +++ b/runsc/cmd/trace/BUILD @@ -9,12 +9,14 @@ go_library( "delete.go", "list.go", "metadata.go", + "procfs.go", "trace.go", ], visibility = [ "//runsc:__subpackages__", ], deps = [ + "//pkg/log", "//pkg/sentry/seccheck", "//runsc/cmd/util", "//runsc/config", diff --git a/runsc/cmd/trace/procfs.go b/runsc/cmd/trace/procfs.go new file mode 100644 index 000000000..11cb808b9 --- /dev/null +++ b/runsc/cmd/trace/procfs.go @@ -0,0 +1,89 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// procfs implements subcommands.Command for the "procfs" command. +type procfs struct { +} + +// Name implements subcommands.Command. +func (*procfs) Name() string { + return "procfs" +} + +// Synopsis implements subcommands.Command. +func (*procfs) Synopsis() string { + return "dump procfs state for sandbox" +} + +// Usage implements subcommands.Command. +func (*procfs) Usage() string { + return `procfs - get procfs dump for a trace session +` +} + +// SetFlags implements subcommands.Command. +func (*procfs) SetFlags(*flag.FlagSet) {} + +// Execute implements subcommands.Command. +func (*procfs) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() != 1 { + f.Usage() + return subcommands.ExitUsageError + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + opts := container.LoadOpts{ + SkipCheck: true, + RootContainer: true, + } + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, opts) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + dump, err := c.Sandbox.ProcfsDump() + if err != nil { + util.Fatalf("procfs dump: %v", err) + } + + fmt.Println("PROCFS DUMP") + for _, procDump := range dump { + out, err := json.Marshal(procDump) + if err != nil { + log.Warningf("json.Marshal failed to marshal %+v: %v", procDump, err) + continue + } + + fmt.Println("") + fmt.Println(string(out)) + } + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/trace/trace.go b/runsc/cmd/trace/trace.go index 45e121107..d5a1d6450 100644 --- a/runsc/cmd/trace/trace.go +++ b/runsc/cmd/trace/trace.go @@ -65,5 +65,6 @@ func createCommander(f *flag.FlagSet) *subcommands.Commander { cdr.Register(new(delete), "") cdr.Register(new(list), "") cdr.Register(new(metadata), "") + cdr.Register(new(procfs), "") return cdr } diff --git a/runsc/container/trace_test.go b/runsc/container/trace_test.go index 20dd883fc..e1091b30f 100644 --- a/runsc/container/trace_test.go +++ b/runsc/container/trace_test.go @@ -297,3 +297,47 @@ func TestTraceForceCreate(t *testing.T) { t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) } } + +func TestProcfsDump(t *testing.T) { + spec, conf := sleepSpecConf(t) + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Create and start the container. + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + cont, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer cont.Destroy() + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + procfsDump, err := cont.Sandbox.ProcfsDump() + if err != nil { + t.Fatalf("ProcfsDump() failed: %v", err) + } + + // Sleep should be the only process running in the container. + if len(procfsDump) != 1 { + t.Fatalf("got incorrect number of proc results: %+v", procfsDump) + } + + // Sleep should be PID 1. + if procfsDump[0].PID != 1 { + t.Errorf("expected sleep process to be pid 1, got %d", procfsDump[0].PID) + } + + // Check that bin/sleep is part of the executable path. + if wantExeSubStr := "bin/sleep"; !strings.HasSuffix(procfsDump[0].Exe, wantExeSubStr) { + t.Errorf("expected %q to be part of execuable path %q", wantExeSubStr, procfsDump[0].Exe) + } +} diff --git a/runsc/sandbox/BUILD b/runsc/sandbox/BUILD index 8c5b6501e..61a89b560 100644 --- a/runsc/sandbox/BUILD +++ b/runsc/sandbox/BUILD @@ -30,6 +30,7 @@ go_library( "//pkg/unet", "//pkg/urpc", "//runsc/boot", + "//runsc/boot/procfs", "//runsc/cgroup", "//runsc/config", "//runsc/console", diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index 79e1d3a83..3ab04f711 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -46,6 +46,7 @@ import ( "gvisor.dev/gvisor/pkg/unet" "gvisor.dev/gvisor/pkg/urpc" "gvisor.dev/gvisor/runsc/boot" + "gvisor.dev/gvisor/runsc/boot/procfs" "gvisor.dev/gvisor/runsc/cgroup" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/console" @@ -453,6 +454,22 @@ func (s *Sandbox) ListTraceSessions() ([]seccheck.SessionConfig, error) { return sessions, nil } +// ProcfsDump collects and returns a procfs dump for the sandbox. +func (s *Sandbox) ProcfsDump() ([]procfs.ProcessProcfsDump, error) { + log.Debugf("Procfs dump %q", s.ID) + conn, err := s.sandboxConnect() + if err != nil { + return nil, err + } + defer conn.Close() + + var procfsDump []procfs.ProcessProcfsDump + if err := conn.Call(boot.ContMgrProcfsDump, nil, &procfsDump); err != nil { + return nil, fmt.Errorf("getting sandbox %q stacks: %v", s.ID, err) + } + return procfsDump, nil +} + // NewCGroup returns the sandbox's Cgroup, or an error if it does not have one. func (s *Sandbox) NewCGroup() (cgroup.Cgroup, error) { return cgroup.NewFromPid(s.Pid.load(), false /* useSystemd */)