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 }