github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/sandbox/cgroup/tracking.go (about)

     1  package cgroup
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/godbus/dbus"
    11  
    12  	"github.com/snapcore/snapd/dbusutil"
    13  	"github.com/snapcore/snapd/logger"
    14  	"github.com/snapcore/snapd/randutil"
    15  )
    16  
    17  var osGetuid = os.Getuid
    18  var osGetpid = os.Getpid
    19  var cgroupProcessPathInTrackingCgroup = ProcessPathInTrackingCgroup
    20  
    21  var ErrCannotTrackProcess = errors.New("cannot track application process")
    22  
    23  // TrackingOptions control how tracking, based on systemd transient scope, operates.
    24  type TrackingOptions struct {
    25  	// AllowSessionBus controls if CreateTransientScopeForTracking will
    26  	// consider using the session bus for making the request.
    27  	AllowSessionBus bool
    28  }
    29  
    30  // CreateTransientScopeForTracking puts the current process in a transient scope.
    31  //
    32  // To quote systemd documentation about scope units:
    33  //
    34  // >> Scopes units manage a set of system processes. Unlike service units,
    35  // >> scope units manage externally created processes, and do not fork off
    36  // >> processes on its own.
    37  //
    38  // Scope names must be unique, a randomly generated UUID is appended to the
    39  // security tag, further suffixed with the string ".scope".
    40  func CreateTransientScopeForTracking(securityTag string, opts *TrackingOptions) error {
    41  	if opts == nil {
    42  		// Retain original semantics when not explicitly configured otherwise.
    43  		opts = &TrackingOptions{AllowSessionBus: true}
    44  	}
    45  	logger.Debugf("creating transient scope %s", securityTag)
    46  
    47  	// Session or system bus might be unavailable. To avoid being fragile
    48  	// ignore all errors when establishing session bus connection to avoid
    49  	// breaking user interactions. This is consistent with similar failure
    50  	// modes below, where other parts of the stack fail.
    51  	//
    52  	// Ideally we would check for a distinct error type but this is just an
    53  	// errors.New() in go-dbus code.
    54  	uid := osGetuid()
    55  	// Depending on options, we may use the session bus instead of the system
    56  	// bus. In addition, when uid == 0 we may fall back from using the session
    57  	// bus to the system bus.
    58  	var isSessionBus bool
    59  	var conn *dbus.Conn
    60  	var err error
    61  	if opts.AllowSessionBus {
    62  		isSessionBus, conn, err = sessionOrMaybeSystemBus(uid)
    63  		if err != nil {
    64  			return ErrCannotTrackProcess
    65  		}
    66  	} else {
    67  		isSessionBus = false
    68  		conn, err = dbusutil.SystemBus()
    69  		if err != nil {
    70  			return ErrCannotTrackProcess
    71  		}
    72  	}
    73  
    74  	// We ask the kernel for a random UUID. We need one because each transient
    75  	// scope needs a unique name. The unique name is composed of said UUID and
    76  	// the snap security tag.
    77  	uuid, err := randomUUID()
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	// Enforcing uniqueness is preferred to reusing an existing scope for
    83  	// simplicity since doing otherwise by joining an existing scope has
    84  	// limitations:
    85  	// - the originally started scope must be marked as a delegate, with all
    86  	//   consequences.
    87  	// - the method AttachProcessesToUnit is unavailable on Ubuntu 16.04
    88  	unitName := fmt.Sprintf("%s.%s.scope", securityTag, uuid)
    89  
    90  	pid := osGetpid()
    91  tryAgain:
    92  	// Create a transient scope by talking to systemd over DBus.
    93  	if err := doCreateTransientScope(conn, unitName, pid); err != nil {
    94  		switch err {
    95  		case errDBusUnknownMethod:
    96  			return ErrCannotTrackProcess
    97  		case errDBusSpawnChildExited:
    98  			fallthrough
    99  		case errDBusNameHasNoOwner:
   100  			if isSessionBus && uid == 0 {
   101  				// We cannot activate systemd --user for root,
   102  				// try the system bus as a fallback.
   103  				logger.Debugf("cannot activate systemd --user on session bus, falling back to system bus: %s", err)
   104  				isSessionBus = false
   105  				conn, err = dbusutil.SystemBus()
   106  				if err != nil {
   107  					logger.Debugf("system bus is not available: %s", err)
   108  					return ErrCannotTrackProcess
   109  				}
   110  				logger.Debugf("using system bus now, session bus could not activate systemd --user")
   111  				goto tryAgain
   112  			}
   113  			return ErrCannotTrackProcess
   114  		}
   115  		return err
   116  	}
   117  	// We may have created a transient scope but due to the constraints the
   118  	// kernel puts on process transitions on unprivileged users (and remember
   119  	// that systemd --user is unprivileged) the actual re-association with the
   120  	// scope cgroup may have silently failed - unfortunately some versions of
   121  	// systemd do not report an error in that case. Systemd 238 and newer
   122  	// detects the error correctly and uses privileged systemd running as pid 1
   123  	// to assist in the transition.
   124  	//
   125  	// For more details about the transition constraints refer to
   126  	// cgroup_procs_write_permission() as of linux 5.8 and
   127  	// unit_attach_pids_to_cgroup() as of systemd 245.
   128  	//
   129  	// Verify the effective tracking cgroup and check that our scope name is
   130  	// contained therein.
   131  	hasTracking := false
   132  	start := time.Now()
   133  	for tries := 0; tries < 100; tries++ {
   134  		path, err := cgroupProcessPathInTrackingCgroup(pid)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		if strings.HasSuffix(path, unitName) {
   139  			hasTracking = true
   140  			break
   141  		}
   142  		time.Sleep(1 * time.Millisecond)
   143  	}
   144  	waitForTracking := time.Since(start)
   145  	logger.Debugf("waited %v for tracking", waitForTracking)
   146  	if !hasTracking {
   147  		logger.Debugf("systemd could not associate process %d with transient scope %s", pid, unitName)
   148  		return ErrCannotTrackProcess
   149  	}
   150  	return nil
   151  }
   152  
   153  // ConfirmSystemdServiceTracking checks if systemd tracks this process as a snap service.
   154  //
   155  // Systemd is placing started services, both user and system, into appropriate
   156  // tracking groups. Given a security tag we can confirm if the current process
   157  // belongs to such tracking group and thus could be identified by snapd as
   158  // belonging to a particular snap and application.
   159  //
   160  // If the application process is not tracked then ErrCannotTrackProcess is returned.
   161  func ConfirmSystemdServiceTracking(securityTag string) error {
   162  	pid := osGetpid()
   163  	path, err := cgroupProcessPathInTrackingCgroup(pid)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	unitName := fmt.Sprintf("%s.service", securityTag)
   168  	if !strings.Contains(path, unitName) {
   169  		return ErrCannotTrackProcess
   170  	}
   171  	return nil
   172  }
   173  
   174  func sessionOrMaybeSystemBus(uid int) (isSessionBus bool, conn *dbus.Conn, err error) {
   175  	// The scope is created with a DBus call to systemd running either on
   176  	// system or session bus. We have a preference for session bus, as this is
   177  	// where applications normally go to. When a session bus is not available
   178  	// and the invoking user is root, we use the system bus instead.
   179  	//
   180  	// It is worth noting that hooks will not normally have a session bus to
   181  	// connect to, as they are invoked as descendants of snapd, and snapd is a
   182  	// service running outside of any session.
   183  	conn, err = dbusutil.SessionBus()
   184  	if err == nil {
   185  		logger.Debugf("using session bus")
   186  		return true, conn, nil
   187  	}
   188  	logger.Debugf("session bus is not available: %s", err)
   189  	if uid == 0 {
   190  		logger.Debugf("falling back to system bus")
   191  		conn, err = dbusutil.SystemBus()
   192  		if err != nil {
   193  			logger.Debugf("system bus is not available: %s", err)
   194  		} else {
   195  			logger.Debugf("using system bus now, session bus was not available")
   196  		}
   197  	}
   198  	return false, conn, err
   199  }
   200  
   201  type handledDBusError struct {
   202  	msg       string
   203  	dbusError string
   204  }
   205  
   206  func (e *handledDBusError) Error() string {
   207  	return fmt.Sprintf("%s [%s]", e.msg, e.dbusError)
   208  }
   209  
   210  var (
   211  	errDBusUnknownMethod    = &handledDBusError{msg: "unknown dbus object method", dbusError: "org.freedesktop.DBus.Error.UnknownMethod"}
   212  	errDBusNameHasNoOwner   = &handledDBusError{msg: "dbus name has no owner", dbusError: "org.freedesktop.DBus.Error.NameHasNoOwner"}
   213  	errDBusSpawnChildExited = &handledDBusError{msg: "dbus spawned child process exited", dbusError: "org.freedesktop.DBus.Error.Spawn.ChildExited"}
   214  )
   215  
   216  // doCreateTransientScope creates a systemd transient scope with specified properties.
   217  //
   218  // The scope is created by asking systemd via the specified DBus connection.
   219  // The unit name and the PID to attach are provided as well. The DBus method
   220  // call is performed outside confinement established by snap-confine.
   221  var doCreateTransientScope = func(conn *dbus.Conn, unitName string, pid int) error {
   222  	// Documentation of StartTransientUnit is available at
   223  	// https://www.freedesktop.org/wiki/Software/systemd/dbus/
   224  	//
   225  	// The property and auxUnit types are not well documented but can be traced
   226  	// from systemd source code. As of systemd 245 it can be found in src/core/dbus-manager.c,
   227  	// in a declaration containing SD_BUS_METHOD_WITH_NAMES("SD_BUS_METHOD_WITH_NAMES",...
   228  	// From there one can follow to method_start_transient_unit to understand
   229  	// how argument parsing is performed.
   230  	//
   231  	// Systemd defines the signature of StartTransientUnit as
   232  	// "ssa(sv)a(sa(sv))". The signature can be decomposed as follows:
   233  	//
   234  	// unitName string // name of the unit to start
   235  	// jobMode string  // corresponds to --job-mode= (see systemctl(1) manual page)
   236  	// properties []struct{
   237  	//   Name string
   238  	//   Value interface{}
   239  	// } // properties describe properties of the started unit
   240  	// auxUnits []struct {
   241  	//   Name string
   242  	//   Properties []struct{
   243  	//   	Name string
   244  	//   	Value interface{}
   245  	//	 }
   246  	// } // auxUnits describe any additional units to define.
   247  	type property struct {
   248  		Name  string
   249  		Value interface{}
   250  	}
   251  	type auxUnit struct {
   252  		Name  string
   253  		Props []property
   254  	}
   255  
   256  	// The mode string decides how the job is interacting with other systemd
   257  	// jobs on the system. The documentation of the systemd StartUnit() method
   258  	// describes the possible values and their properties:
   259  	//
   260  	// >> StartUnit() enqeues a start job, and possibly depending jobs. Takes
   261  	// >> the unit to activate, plus a mode string. The mode needs to be one of
   262  	// >> replace, fail, isolate, ignore-dependencies, ignore-requirements. If
   263  	// >> "replace" the call will start the unit and its dependencies, possibly
   264  	// >> replacing already queued jobs that conflict with this. If "fail" the
   265  	// >> call will start the unit and its dependencies, but will fail if this
   266  	// >> would change an already queued job. If "isolate" the call will start
   267  	// >> the unit in question and terminate all units that aren't dependencies
   268  	// >> of it. If "ignore-dependencies" it will start a unit but ignore all
   269  	// >> its dependencies. If "ignore-requirements" it will start a unit but
   270  	// >> only ignore the requirement dependencies. It is not recommended to
   271  	// >> make use of the latter two options. Returns the newly created job
   272  	// >> object.
   273  	//
   274  	// Here we choose "fail" to match systemd-run.
   275  	mode := "fail"
   276  	properties := []property{{"PIDs", []uint{uint(pid)}}}
   277  	aux := []auxUnit(nil)
   278  	systemd := conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
   279  	call := systemd.Call(
   280  		"org.freedesktop.systemd1.Manager.StartTransientUnit",
   281  		0,
   282  		unitName,
   283  		mode,
   284  		properties,
   285  		aux,
   286  	)
   287  	var job dbus.ObjectPath
   288  	if err := call.Store(&job); err != nil {
   289  		if dbusErr, ok := err.(dbus.Error); ok {
   290  			logger.Debugf("StartTransientUnit failed with %q: %v", dbusErr.Name, dbusErr.Body)
   291  			// Some specific DBus errors have distinct handling.
   292  			switch dbusErr.Name {
   293  			case "org.freedesktop.DBus.Error.NameHasNoOwner":
   294  				// Nothing is providing systemd bus name. This is, most likely,
   295  				// an Ubuntu 14.04 system with the special deputy systemd.
   296  				return errDBusNameHasNoOwner
   297  			case "org.freedesktop.DBus.Error.UnknownMethod":
   298  				// The DBus API is not supported on this system. This can happen on
   299  				// very old versions of Systemd, for instance on Ubuntu 14.04.
   300  				return errDBusUnknownMethod
   301  			case "org.freedesktop.DBus.Error.Spawn.ChildExited":
   302  				// We tried to socket-activate dbus-daemon or bus-activate
   303  				// systemd --user but it failed.
   304  				return errDBusSpawnChildExited
   305  			case "org.freedesktop.systemd1.UnitExists":
   306  				// Starting a scope with a name that already exists is an
   307  				// error. Normally this should never happen.
   308  				return fmt.Errorf("cannot create transient scope: scope %q clashed: %s", unitName, err)
   309  			default:
   310  				return fmt.Errorf("cannot create transient scope: DBus error %q: %v", dbusErr.Name, dbusErr.Body)
   311  			}
   312  		}
   313  		if err != nil {
   314  			return fmt.Errorf("cannot create transient scope: %s", err)
   315  		}
   316  	}
   317  	logger.Debugf("created transient scope as object: %s", job)
   318  	return nil
   319  }
   320  
   321  var randomUUID = func() (string, error) {
   322  	// The source of the bytes generated here is the same as that of
   323  	// /dev/urandom which doesn't block and is sufficient for our purposes
   324  	// of avoiding clashing UUIDs that are needed for all of the non-service
   325  	// commands that are started with the help of this UUID.
   326  	return randutil.RandomKernelUUID(), nil
   327  }