From 6fdeba8d7840f110ee36ef877ed6384674349868 Mon Sep 17 00:00:00 2001 From: Ayush Ranjan Date: Thu, 19 May 2022 11:27:56 -0700 Subject: [PATCH] Add support for /prod/[pid]/cmdline and /proc/[pid]/environ to trace procfs. This change exports some logic in fsimpl/proc so that runsc/boot can use it. Added tests for these fields. Updates #4805 PiperOrigin-RevId: 449794552 --- pkg/sentry/fsimpl/proc/task.go | 4 +- pkg/sentry/fsimpl/proc/task_files.go | 97 +++++++++++++++------------- runsc/boot/procfs/BUILD | 1 + runsc/boot/procfs/dump.go | 25 ++++++- runsc/container/trace_test.go | 20 ++++++ 5 files changed, 99 insertions(+), 48 deletions(-) diff --git a/pkg/sentry/fsimpl/proc/task.go b/pkg/sentry/fsimpl/proc/task.go index f54811edf..a496b7221 100644 --- a/pkg/sentry/fsimpl/proc/task.go +++ b/pkg/sentry/fsimpl/proc/task.go @@ -54,10 +54,10 @@ func (fs *filesystem) newTaskInode(ctx context.Context, task *kernel.Task, pidns contents := map[string]kernfs.Inode{ "auxv": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &auxvData{task: task}), - "cmdline": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &cmdlineData{task: task, arg: cmdlineDataArg}), + "cmdline": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &metadataData{task: task, metaType: Cmdline}), "comm": fs.newComm(ctx, task, fs.NextIno(), 0444), "cwd": fs.newCwdSymlink(ctx, task, fs.NextIno()), - "environ": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &cmdlineData{task: task, arg: environDataArg}), + "environ": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &metadataData{task: task, metaType: Environ}), "exe": fs.newExeSymlink(ctx, task, fs.NextIno()), "fd": fs.newFDDirInode(ctx, task), "fdinfo": fs.newFDInfoDirInode(ctx, task), diff --git a/pkg/sentry/fsimpl/proc/task_files.go b/pkg/sentry/fsimpl/proc/task_files.go index 368f96683..ce8e0581f 100644 --- a/pkg/sentry/fsimpl/proc/task_files.go +++ b/pkg/sentry/fsimpl/proc/task_files.go @@ -133,56 +133,35 @@ func (d *auxvData) Generate(ctx context.Context, buf *bytes.Buffer) error { return nil } -// execArgType enumerates the types of exec arguments that are exposed through -// proc. -type execArgType int +// MetadataType enumerates the types of metadata that is exposed through proc. +type MetadataType int const ( - cmdlineDataArg execArgType = iota - environDataArg + // Cmdline represents /proc/[pid]/cmdline. + Cmdline MetadataType = iota + + // Environ represents /proc/[pid]/environ. + Environ ) -// cmdlineData implements vfs.DynamicBytesSource for /proc/[pid]/cmdline. -// -// +stateify savable -type cmdlineData struct { - kernfs.DynamicBytesFile - - task *kernel.Task - - // arg is the type of exec argument this file contains. - arg execArgType -} - -var _ dynamicInode = (*cmdlineData)(nil) - -// Generate implements vfs.DynamicBytesSource.Generate. -func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { - if d.task.ExitState() == kernel.TaskExitDead { - return linuxerr.ESRCH - } - m, err := getMMIncRef(d.task) - if err != nil { - // Return empty file. - return nil - } - defer m.DecUsers(ctx) - +// GetMetadata fetches the process's metadata of type t and writes it into +// buf. The process is identified by mm. +func GetMetadata(ctx context.Context, mm *mm.MemoryManager, buf *bytes.Buffer, t MetadataType) error { // Figure out the bounds of the exec arg we are trying to read. var ar hostarch.AddrRange - switch d.arg { - case cmdlineDataArg: + switch t { + case Cmdline: ar = hostarch.AddrRange{ - Start: m.ArgvStart(), - End: m.ArgvEnd(), + Start: mm.ArgvStart(), + End: mm.ArgvEnd(), } - case environDataArg: + case Environ: ar = hostarch.AddrRange{ - Start: m.EnvvStart(), - End: m.EnvvEnd(), + Start: mm.EnvvStart(), + End: mm.EnvvEnd(), } default: - panic(fmt.Sprintf("unknown exec arg type %v", d.arg)) + panic(fmt.Sprintf("unknown exec arg type %v", t)) } if ar.Start == 0 || ar.End == 0 { // Don't attempt to read before the start/end are set up. @@ -193,7 +172,7 @@ func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { // until Linux 4.9 (272ddc8b3735 "proc: don't use FOLL_FORCE for reading // cmdline and environment"). writer := &bufferWriter{buf: buf} - if n, err := m.CopyInTo(ctx, hostarch.AddrRangeSeqOf(ar), writer, usermem.IOOpts{}); n == 0 || err != nil { + if n, err := mm.CopyInTo(ctx, hostarch.AddrRangeSeqOf(ar), writer, usermem.IOOpts{}); n == 0 || err != nil { // Nothing to copy or something went wrong. return err } @@ -201,7 +180,7 @@ func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { // On Linux, if the NULL byte at the end of the argument vector has been // overwritten, it continues reading the environment vector as part of // the argument vector. - if d.arg == cmdlineDataArg && buf.Bytes()[buf.Len()-1] != 0 { + if t == Cmdline && buf.Bytes()[buf.Len()-1] != 0 { if end := bytes.IndexByte(buf.Bytes(), 0); end != -1 { // If we found a NULL character somewhere else in argv, truncate the // return up to the NULL terminator (including it). @@ -211,8 +190,8 @@ func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { // There is no NULL terminator in the string, return into envp. arEnvv := hostarch.AddrRange{ - Start: m.EnvvStart(), - End: m.EnvvEnd(), + Start: mm.EnvvStart(), + End: mm.EnvvEnd(), } // Upstream limits the returned amount to one page of slop. @@ -231,7 +210,7 @@ func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { } arEnvv.End = end } - if _, err := m.CopyInTo(ctx, hostarch.AddrRangeSeqOf(arEnvv), writer, usermem.IOOpts{}); err != nil { + if _, err := mm.CopyInTo(ctx, hostarch.AddrRangeSeqOf(arEnvv), writer, usermem.IOOpts{}); err != nil { return err } @@ -246,6 +225,36 @@ func (d *cmdlineData) Generate(ctx context.Context, buf *bytes.Buffer) error { return nil } +// metadataData implements vfs.DynamicBytesSource for proc metadata fields like: +// - /proc/[pid]/cmdline +// - /proc/[pid]/environ +// +// +stateify savable +type metadataData struct { + kernfs.DynamicBytesFile + + task *kernel.Task + + // arg is the type of exec argument this file contains. + metaType MetadataType +} + +var _ dynamicInode = (*metadataData)(nil) + +// Generate implements vfs.DynamicBytesSource.Generate. +func (d *metadataData) Generate(ctx context.Context, buf *bytes.Buffer) error { + if d.task.ExitState() == kernel.TaskExitDead { + return linuxerr.ESRCH + } + m, err := getMMIncRef(d.task) + if err != nil { + // Return empty file. + return nil + } + defer m.DecUsers(ctx) + return GetMetadata(ctx, m, buf, d.metaType) +} + // +stateify savable type commInode struct { kernfs.DynamicBytesFile diff --git a/runsc/boot/procfs/BUILD b/runsc/boot/procfs/BUILD index 1221423d7..82ce12fa8 100644 --- a/runsc/boot/procfs/BUILD +++ b/runsc/boot/procfs/BUILD @@ -9,6 +9,7 @@ go_library( deps = [ "//pkg/context", "//pkg/log", + "//pkg/sentry/fsimpl/proc", "//pkg/sentry/kernel", "//pkg/sentry/mm", ], diff --git a/runsc/boot/procfs/dump.go b/runsc/boot/procfs/dump.go index d39bc743f..81a386271 100644 --- a/runsc/boot/procfs/dump.go +++ b/runsc/boot/procfs/dump.go @@ -17,10 +17,13 @@ package procfs import ( + "bytes" "fmt" + "strings" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/proc" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/mm" ) @@ -31,6 +34,10 @@ type ProcessProcfsDump struct { PID int32 `json:"pid,omitempty"` // Exe is the symlink target of /proc/[pid]/exe. Exe string `json:"exe,omitempty"` + // Args is /proc/[pid]/cmdline split into an array. + Args []string `json:"args,omitempty"` + // Env is /proc/[pid]/environ split into an array. + Env []string `json:"env,omitempty"` } // getMM returns t's MemoryManager. On success, the MemoryManager's users count @@ -58,6 +65,18 @@ func getExecutablePath(ctx context.Context, pid kernel.ThreadID, mm *mm.MemoryMa return exec.PathnameWithDeleted(ctx) } +func getMetadataArray(ctx context.Context, pid kernel.ThreadID, mm *mm.MemoryManager, metaType proc.MetadataType) []string { + buf := bytes.Buffer{} + if err := proc.GetMetadata(ctx, mm, &buf, metaType); err != nil { + log.Warningf("failed to get %v metadata for PID %s: %v", metaType, pid, err) + return nil + } + // As per proc(5), /proc/[pid]/cmdline may have "a further null byte after + // the last string". Similarly, for /proc/[pid]/environ "there may be a null + // byte at the end". So trim off the last null byte if it exists. + return strings.Split(strings.TrimSuffix(buf.String(), "\000"), "\000") +} + // 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() @@ -69,7 +88,9 @@ func Dump(t *kernel.Task, pid kernel.ThreadID) (ProcessProcfsDump, error) { defer mm.DecUsers(ctx) return ProcessProcfsDump{ - PID: int32(pid), - Exe: getExecutablePath(ctx, pid, mm), + PID: int32(pid), + Exe: getExecutablePath(ctx, pid, mm), + Args: getMetadataArray(ctx, pid, mm, proc.Cmdline), + Env: getMetadataArray(ctx, pid, mm, proc.Environ), }, nil } diff --git a/runsc/container/trace_test.go b/runsc/container/trace_test.go index e1091b30f..1bb4a9469 100644 --- a/runsc/container/trace_test.go +++ b/runsc/container/trace_test.go @@ -300,6 +300,8 @@ func TestTraceForceCreate(t *testing.T) { func TestProcfsDump(t *testing.T) { spec, conf := sleepSpecConf(t) + testEnv := "GVISOR_IS_GREAT=true" + spec.Process.Env = append(spec.Process.Env, testEnv) _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) if err != nil { t.Fatalf("error setting up container: %v", err) @@ -340,4 +342,22 @@ func TestProcfsDump(t *testing.T) { 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) } + + if len(procfsDump[0].Args) != 2 { + t.Errorf("expected 2 args, but got %+v", procfsDump[0].Args) + } else { + if procfsDump[0].Args[0] != "sleep" || procfsDump[0].Args[1] != "1000" { + t.Errorf("expected args %q but got %+v", "sleep 1000", procfsDump[0].Args) + } + } + + testEnvFound := false + for _, env := range procfsDump[0].Env { + if env == testEnv { + testEnvFound = true + } + } + if !testEnvFound { + t.Errorf("expected to find %q env but did not find it, got env %+v", testEnv, procfsDump[0].Env) + } }