github.com/nicocha30/gvisor-ligolo@v0.0.0-20230726075806-989fa2c0a413/pkg/sentry/control/proc.go (about)

     1  // Copyright 2018 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 control
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"sort"
    23  	"strings"
    24  	"text/tabwriter"
    25  	"time"
    26  
    27  	"github.com/nicocha30/gvisor-ligolo/pkg/abi/linux"
    28  	"github.com/nicocha30/gvisor-ligolo/pkg/fd"
    29  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/fdimport"
    30  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/fsimpl/host"
    31  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/fsimpl/user"
    32  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/kernel"
    33  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/kernel/auth"
    34  	ktime "github.com/nicocha30/gvisor-ligolo/pkg/sentry/kernel/time"
    35  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/limits"
    36  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/usage"
    37  	"github.com/nicocha30/gvisor-ligolo/pkg/sentry/vfs"
    38  	"github.com/nicocha30/gvisor-ligolo/pkg/urpc"
    39  )
    40  
    41  // Proc includes task-related functions.
    42  //
    43  // At the moment, this is limited to exec support.
    44  type Proc struct {
    45  	Kernel *kernel.Kernel
    46  }
    47  
    48  // FilePayload aids to ensure that payload files and guest file descriptors are
    49  // consistent when instantiated through the NewFilePayload helper method.
    50  type FilePayload struct {
    51  	// FilePayload is the file payload that is transferred via RPC.
    52  	urpc.FilePayload
    53  
    54  	// GuestFDs are the file descriptors in the file descriptor map of the
    55  	// executed application. They correspond 1:1 to the files in the
    56  	// urpc.FilePayload. If a program is executed from a host file descriptor,
    57  	// the file payload may contain one additional file. In that case, the file
    58  	// used for program execution is the last file in the Files array.
    59  	GuestFDs []int
    60  }
    61  
    62  // NewFilePayload returns a FilePayload that maps file descriptors to files inside
    63  // the executed process and provides a file for execution.
    64  func NewFilePayload(fdMap map[int]*os.File, execFile *os.File) FilePayload {
    65  	fileCount := len(fdMap)
    66  	if execFile != nil {
    67  		fileCount++
    68  	}
    69  	files := make([]*os.File, 0, fileCount)
    70  	guestFDs := make([]int, 0, len(fdMap))
    71  
    72  	// Make the map iteration order deterministic for the sake of testing.
    73  	// Otherwise, the order is randomized and tests relying on the comparison
    74  	// of equality will fail.
    75  	for key := range fdMap {
    76  		guestFDs = append(guestFDs, key)
    77  	}
    78  	sort.Ints(guestFDs)
    79  
    80  	for _, guestFD := range guestFDs {
    81  		files = append(files, fdMap[guestFD])
    82  	}
    83  
    84  	if execFile != nil {
    85  		files = append(files, execFile)
    86  	}
    87  
    88  	return FilePayload{
    89  		FilePayload: urpc.FilePayload{Files: files},
    90  		GuestFDs:    guestFDs,
    91  	}
    92  }
    93  
    94  // ExecArgs is the set of arguments to exec.
    95  type ExecArgs struct {
    96  	// Filename is the filename to load.
    97  	//
    98  	// If this is provided as "", then the file will be guessed via Argv[0].
    99  	Filename string `json:"filename"`
   100  
   101  	// Argv is a list of arguments.
   102  	Argv []string `json:"argv"`
   103  
   104  	// Envv is a list of environment variables.
   105  	Envv []string `json:"envv"`
   106  
   107  	// MountNamespace is the mount namespace to execute the new process in.
   108  	// A reference on MountNamespace must be held for the lifetime of the
   109  	// ExecArgs. If MountNamespace is nil, it will default to the init
   110  	// process's MountNamespace.
   111  	MountNamespace *vfs.MountNamespace
   112  
   113  	// WorkingDirectory defines the working directory for the new process.
   114  	WorkingDirectory string `json:"wd"`
   115  
   116  	// KUID is the UID to run with in the root user namespace. Defaults to
   117  	// root if not set explicitly.
   118  	KUID auth.KUID
   119  
   120  	// KGID is the GID to run with in the root user namespace. Defaults to
   121  	// the root group if not set explicitly.
   122  	KGID auth.KGID
   123  
   124  	// ExtraKGIDs is the list of additional groups to which the user belongs.
   125  	ExtraKGIDs []auth.KGID
   126  
   127  	// Capabilities is the list of capabilities to give to the process.
   128  	Capabilities *auth.TaskCapabilities
   129  
   130  	// StdioIsPty indicates that FDs 0, 1, and 2 are connected to a host pty FD.
   131  	StdioIsPty bool
   132  
   133  	// FilePayload determines the files to give to the new process.
   134  	FilePayload
   135  
   136  	// ContainerID is the container for the process being executed.
   137  	ContainerID string
   138  
   139  	// PIDNamespace is the pid namespace for the process being executed.
   140  	PIDNamespace *kernel.PIDNamespace
   141  
   142  	// Limits is the limit set for the process being executed.
   143  	Limits *limits.LimitSet
   144  }
   145  
   146  // String prints the arguments as a string.
   147  func (args *ExecArgs) String() string {
   148  	if len(args.Argv) == 0 {
   149  		return args.Filename
   150  	}
   151  	a := make([]string, len(args.Argv))
   152  	copy(a, args.Argv)
   153  	if args.Filename != "" {
   154  		a[0] = args.Filename
   155  	}
   156  	return strings.Join(a, " ")
   157  }
   158  
   159  // Exec runs a new task.
   160  func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error {
   161  	newTG, _, _, err := proc.execAsync(args)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	// Wait for completion.
   167  	newTG.WaitExited()
   168  	*waitStatus = uint32(newTG.ExitStatus())
   169  	return nil
   170  }
   171  
   172  // ExecAsync runs a new task, but doesn't wait for it to finish. It is defined
   173  // as a function rather than a method to avoid exposing execAsync as an RPC.
   174  func ExecAsync(proc *Proc, args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, *host.TTYFileDescription, error) {
   175  	return proc.execAsync(args)
   176  }
   177  
   178  // execAsync runs a new task, but doesn't wait for it to finish. It returns the
   179  // newly created thread group and its PID. If the stdio FDs are TTYs, then a
   180  // TTYFileOperations that wraps the TTY is also returned.
   181  func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, *host.TTYFileDescription, error) {
   182  	// Import file descriptors.
   183  	fdTable := proc.Kernel.NewFDTable()
   184  
   185  	creds := auth.NewUserCredentials(
   186  		args.KUID,
   187  		args.KGID,
   188  		args.ExtraKGIDs,
   189  		args.Capabilities,
   190  		proc.Kernel.RootUserNamespace())
   191  
   192  	pidns := args.PIDNamespace
   193  	if pidns == nil {
   194  		pidns = proc.Kernel.RootPIDNamespace()
   195  	}
   196  	limitSet := args.Limits
   197  	if limitSet == nil {
   198  		limitSet = limits.NewLimitSet()
   199  	}
   200  	initArgs := kernel.CreateProcessArgs{
   201  		Filename:                args.Filename,
   202  		Argv:                    args.Argv,
   203  		Envv:                    args.Envv,
   204  		WorkingDirectory:        args.WorkingDirectory,
   205  		MountNamespace:          args.MountNamespace,
   206  		Credentials:             creds,
   207  		FDTable:                 fdTable,
   208  		Umask:                   0022,
   209  		Limits:                  limitSet,
   210  		MaxSymlinkTraversals:    linux.MaxSymlinkTraversals,
   211  		UTSNamespace:            proc.Kernel.RootUTSNamespace(),
   212  		IPCNamespace:            proc.Kernel.RootIPCNamespace(),
   213  		AbstractSocketNamespace: proc.Kernel.RootAbstractSocketNamespace(),
   214  		ContainerID:             args.ContainerID,
   215  		PIDNamespace:            pidns,
   216  	}
   217  	if initArgs.MountNamespace != nil {
   218  		// initArgs must hold a reference on MountNamespace, which will
   219  		// be donated to the new process in CreateProcess.
   220  		initArgs.MountNamespace.IncRef()
   221  	}
   222  	ctx := initArgs.NewContext(proc.Kernel)
   223  	defer fdTable.DecRef(ctx)
   224  
   225  	// Get the full path to the filename from the PATH env variable.
   226  	if initArgs.MountNamespace == nil {
   227  		// Set initArgs so that 'ctx' returns the namespace.
   228  		//
   229  		// Add a reference to the namespace, which is transferred to the new process.
   230  		initArgs.MountNamespace = proc.Kernel.GlobalInit().Leader().MountNamespace()
   231  		initArgs.MountNamespace.IncRef()
   232  	}
   233  
   234  	fdMap, execFD, err := args.unpackFiles()
   235  	if err != nil {
   236  		return nil, 0, nil, fmt.Errorf("creating fd map: %w", err)
   237  	}
   238  	defer func() {
   239  		for _, hostFD := range fdMap {
   240  			_ = hostFD.Close()
   241  		}
   242  	}()
   243  
   244  	if execFD != nil {
   245  		if initArgs.Filename != "" {
   246  			return nil, 0, nil, fmt.Errorf("process must either be started from a file or a filename, not both")
   247  		}
   248  		file, err := host.NewFD(ctx, proc.Kernel.HostMount(), execFD.FD(), &host.NewFDOptions{
   249  			Readonly:     true,
   250  			Savable:      true,
   251  			VirtualOwner: true,
   252  			UID:          args.KUID,
   253  			GID:          args.KGID,
   254  		})
   255  		if err != nil {
   256  			return nil, 0, nil, err
   257  		}
   258  		defer file.DecRef(ctx)
   259  		execFD.Release()
   260  		initArgs.File = file
   261  	} else {
   262  		resolved, err := user.ResolveExecutablePath(ctx, &initArgs)
   263  		if err != nil {
   264  			return nil, 0, nil, err
   265  		}
   266  		initArgs.Filename = resolved
   267  	}
   268  
   269  	ttyFile, err := fdimport.Import(ctx, fdTable, args.StdioIsPty, args.KUID, args.KGID, fdMap)
   270  	if err != nil {
   271  		return nil, 0, nil, err
   272  	}
   273  
   274  	tg, tid, err := proc.Kernel.CreateProcess(initArgs)
   275  	if err != nil {
   276  		return nil, 0, nil, err
   277  	}
   278  
   279  	// Set the foreground process group on the TTY before starting the process.
   280  	if ttyFile != nil {
   281  		ttyFile.InitForegroundProcessGroup(tg.ProcessGroup())
   282  	}
   283  
   284  	// Start the newly created process.
   285  	proc.Kernel.StartProcess(tg)
   286  
   287  	return tg, tid, ttyFile, nil
   288  }
   289  
   290  // PsArgs is the set of arguments to ps.
   291  type PsArgs struct {
   292  	// JSON will force calls to Ps to return the result as a JSON payload.
   293  	JSON bool
   294  }
   295  
   296  // Ps provides a process listing for the running kernel.
   297  func (proc *Proc) Ps(args *PsArgs, out *string) error {
   298  	var p []*Process
   299  	if e := Processes(proc.Kernel, "", &p); e != nil {
   300  		return e
   301  	}
   302  	if !args.JSON {
   303  		*out = ProcessListToTable(p)
   304  	} else {
   305  		s, e := ProcessListToJSON(p)
   306  		if e != nil {
   307  			return e
   308  		}
   309  		*out = s
   310  	}
   311  	return nil
   312  }
   313  
   314  // Process contains information about a single process in a Sandbox.
   315  type Process struct {
   316  	UID auth.KUID       `json:"uid"`
   317  	PID kernel.ThreadID `json:"pid"`
   318  	// Parent PID
   319  	PPID    kernel.ThreadID   `json:"ppid"`
   320  	Threads []kernel.ThreadID `json:"threads"`
   321  	// Processor utilization
   322  	C int32 `json:"c"`
   323  	// TTY name of the process. Will be of the form "pts/N" if there is a
   324  	// TTY, or "?" if there is not.
   325  	TTY string `json:"tty"`
   326  	// Start time
   327  	STime string `json:"stime"`
   328  	// CPU time
   329  	Time string `json:"time"`
   330  	// Executable shortname (e.g. "sh" for /bin/sh)
   331  	Cmd string `json:"cmd"`
   332  }
   333  
   334  // ProcessListToTable prints a table with the following format:
   335  // UID       PID       PPID      C         TTY		STIME     TIME       CMD
   336  // 0         1         0         0         pty/4	14:04     505262ns   tail
   337  func ProcessListToTable(pl []*Process) string {
   338  	var buf bytes.Buffer
   339  	tw := tabwriter.NewWriter(&buf, 10, 1, 3, ' ', 0)
   340  	fmt.Fprint(tw, "UID\tPID\tPPID\tC\tTTY\tSTIME\tTIME\tCMD")
   341  	for _, d := range pl {
   342  		fmt.Fprintf(tw, "\n%d\t%d\t%d\t%d\t%s\t%s\t%s\t%s",
   343  			d.UID,
   344  			d.PID,
   345  			d.PPID,
   346  			d.C,
   347  			d.TTY,
   348  			d.STime,
   349  			d.Time,
   350  			d.Cmd)
   351  	}
   352  	tw.Flush()
   353  	return buf.String()
   354  }
   355  
   356  // ProcessListToJSON will return the JSON representation of ps.
   357  func ProcessListToJSON(pl []*Process) (string, error) {
   358  	b, err := json.MarshalIndent(pl, "", "  ")
   359  	if err != nil {
   360  		return "", fmt.Errorf("couldn't marshal process list %v: %v", pl, err)
   361  	}
   362  	return string(b), nil
   363  }
   364  
   365  // PrintPIDsJSON prints a JSON object containing only the PIDs in pl. This
   366  // behavior is the same as runc's.
   367  func PrintPIDsJSON(pl []*Process) (string, error) {
   368  	pids := make([]kernel.ThreadID, 0, len(pl))
   369  	for _, d := range pl {
   370  		pids = append(pids, d.PID)
   371  	}
   372  	b, err := json.Marshal(pids)
   373  	if err != nil {
   374  		return "", fmt.Errorf("couldn't marshal PIDs %v: %v", pids, err)
   375  	}
   376  	return string(b), nil
   377  }
   378  
   379  // Processes retrieves information about processes running in the sandbox with
   380  // the given container id. All processes are returned if 'containerID' is empty.
   381  func Processes(k *kernel.Kernel, containerID string, out *[]*Process) error {
   382  	ts := k.TaskSet()
   383  	now := k.RealtimeClock().Now()
   384  	pidns := ts.Root
   385  	for _, tg := range pidns.ThreadGroups() {
   386  		pid := pidns.IDOfThreadGroup(tg)
   387  
   388  		// If tg has already been reaped ignore it.
   389  		if pid == 0 {
   390  			continue
   391  		}
   392  		if containerID != "" && containerID != tg.Leader().ContainerID() {
   393  			continue
   394  		}
   395  
   396  		ppid := kernel.ThreadID(0)
   397  		if p := tg.Leader().Parent(); p != nil {
   398  			ppid = pidns.IDOfThreadGroup(p.ThreadGroup())
   399  		}
   400  		threads := tg.MemberIDs(pidns)
   401  		*out = append(*out, &Process{
   402  			UID:     tg.Leader().Credentials().EffectiveKUID,
   403  			PID:     pid,
   404  			PPID:    ppid,
   405  			Threads: threads,
   406  			STime:   formatStartTime(now, tg.Leader().StartTime()),
   407  			C:       percentCPU(tg.CPUStats(), tg.Leader().StartTime(), now),
   408  			Time:    tg.CPUStats().SysTime.String(),
   409  			Cmd:     tg.Leader().Name(),
   410  			TTY:     ttyName(tg.TTY()),
   411  		})
   412  	}
   413  	sort.Slice(*out, func(i, j int) bool { return (*out)[i].PID < (*out)[j].PID })
   414  	return nil
   415  }
   416  
   417  // formatStartTime formats startTime depending on the current time:
   418  //   - If startTime was today, HH:MM is used.
   419  //   - If startTime was not today but was this year, MonDD is used (e.g. Jan02)
   420  //   - If startTime was not this year, the year is used.
   421  func formatStartTime(now, startTime ktime.Time) string {
   422  	nowS, nowNs := now.Unix()
   423  	n := time.Unix(nowS, nowNs)
   424  	startTimeS, startTimeNs := startTime.Unix()
   425  	st := time.Unix(startTimeS, startTimeNs)
   426  	format := "15:04"
   427  	if st.YearDay() != n.YearDay() {
   428  		format = "Jan02"
   429  	}
   430  	if st.Year() != n.Year() {
   431  		format = "2006"
   432  	}
   433  	return st.Format(format)
   434  }
   435  
   436  func percentCPU(stats usage.CPUStats, startTime, now ktime.Time) int32 {
   437  	// Note: In procps, there is an option to include child CPU stats. As
   438  	// it is disabled by default, we do not include them.
   439  	total := stats.UserTime + stats.SysTime
   440  	lifetime := now.Sub(startTime)
   441  	if lifetime <= 0 {
   442  		return 0
   443  	}
   444  	percentCPU := total * 100 / lifetime
   445  	// Cap at 99% since procps does the same.
   446  	if percentCPU > 99 {
   447  		percentCPU = 99
   448  	}
   449  	return int32(percentCPU)
   450  }
   451  
   452  func ttyName(tty *kernel.TTY) string {
   453  	if tty == nil {
   454  		return "?"
   455  	}
   456  	return fmt.Sprintf("pts/%d", tty.Index)
   457  }
   458  
   459  // ContainerUsage retrieves per-container CPU usage.
   460  func ContainerUsage(kr *kernel.Kernel) map[string]uint64 {
   461  	cusage := make(map[string]uint64)
   462  	for _, tg := range kr.TaskSet().Root.ThreadGroups() {
   463  		// We want each tg's usage including reaped children.
   464  		cid := tg.Leader().ContainerID()
   465  		stats := tg.CPUStats()
   466  		stats.Accumulate(tg.JoinedChildCPUStats())
   467  		cusage[cid] += uint64(stats.UserTime.Nanoseconds()) + uint64(stats.SysTime.Nanoseconds())
   468  	}
   469  	return cusage
   470  }
   471  
   472  // unpackFiles unpacks the file descriptor map and, if applicable, the file
   473  // descriptor to be used for execution from the unmarshalled ExecArgs.
   474  func (args *ExecArgs) unpackFiles() (map[int]*fd.FD, *fd.FD, error) {
   475  	var execFD *fd.FD
   476  	var err error
   477  
   478  	// If there is one additional file, the last file is used for program
   479  	// execution.
   480  	if len(args.Files) == len(args.GuestFDs)+1 {
   481  		execFD, err = fd.NewFromFile(args.Files[len(args.Files)-1])
   482  		if err != nil {
   483  			return nil, nil, fmt.Errorf("duplicating exec file: %w", err)
   484  		}
   485  	} else if len(args.Files) != len(args.GuestFDs) {
   486  		return nil, nil, fmt.Errorf("length of payload files does not match length of file descriptor array")
   487  	}
   488  
   489  	// GuestFDs are the indexes of our FD map.
   490  	fdMap := make(map[int]*fd.FD, len(args.GuestFDs))
   491  	for i, appFD := range args.GuestFDs {
   492  		file := args.Files[i]
   493  		if appFD < 0 {
   494  			return nil, nil, fmt.Errorf("guest file descriptors must be 0 or greater")
   495  		}
   496  		hostFD, err := fd.NewFromFile(file)
   497  		if err != nil {
   498  			return nil, nil, fmt.Errorf("duplicating payload files: %w", err)
   499  		}
   500  		fdMap[appFD] = hostFD
   501  	}
   502  	return fdMap, execFD, nil
   503  }