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