github.com/rigado/snapd@v2.42.5-go-mod+incompatible/interfaces/apparmor/spec.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package apparmor 21 22 import ( 23 "bytes" 24 "fmt" 25 "path/filepath" 26 "sort" 27 "strings" 28 29 "github.com/snapcore/snapd/interfaces" 30 "github.com/snapcore/snapd/snap" 31 "github.com/snapcore/snapd/strutil" 32 ) 33 34 // Specification assists in collecting apparmor entries associated with an interface. 35 type Specification struct { 36 // scope for various Add{...}Snippet functions 37 securityTags []string 38 39 // snippets are indexed by security tag and describe parts of apparmor policy 40 // for snap application and hook processes. The security tag encodes the identity 41 // of the application or hook. 42 snippets map[string][]string 43 // updateNS describe parts of apparmor policy for snap-update-ns executing 44 // on behalf of a given snap. 45 updateNS strutil.OrderedSet 46 47 // AppArmor deny rules cannot be undone by allow rules which makes 48 // deny rules difficult to work with arbitrary combinations of 49 // interfaces. Sometimes it is useful to suppress noisy denials and 50 // because that can currently only be done with explicit deny rules, 51 // adding explicit deny rules unconditionally makes it difficult for 52 // interfaces to be used in combination. Define the suppressPtraceTrace 53 // to allow an interface to request suppression and define 54 // usesPtraceTrace to omit the explicit deny rules such that: 55 // if suppressPtraceTrace && !usesPtraceTrace { 56 // add 'deny ptrace (trace),' 57 // } 58 suppressPtraceTrace bool 59 usesPtraceTrace bool 60 61 // The home interface typically should have 'ix' as part of its rules, 62 // but specifying certain change_profile rules with these rules cases 63 // a 'conflicting x modifiers' parser error. Allow interfaces that 64 // require this type of change_profile rule to suppress 'ix' so that 65 // the calling interface can be used with the home interface. Ideally, 66 // we would not need this, but we currently do (LP: #1797786) 67 suppressHomeIx bool 68 } 69 70 // setScope sets the scope of subsequent AddSnippet family functions. 71 // The returned function resets the scope to an empty scope. 72 func (spec *Specification) setScope(securityTags []string) (restore func()) { 73 spec.securityTags = securityTags 74 return func() { 75 spec.securityTags = nil 76 } 77 } 78 79 // AddSnippet adds a new apparmor snippet to all applications and hooks using the interface. 80 func (spec *Specification) AddSnippet(snippet string) { 81 if len(spec.securityTags) == 0 { 82 return 83 } 84 if spec.snippets == nil { 85 spec.snippets = make(map[string][]string) 86 } 87 for _, tag := range spec.securityTags { 88 spec.snippets[tag] = append(spec.snippets[tag], snippet) 89 sort.Strings(spec.snippets[tag]) 90 } 91 } 92 93 // AddUpdateNS adds a new apparmor snippet for the snap-update-ns program. 94 func (spec *Specification) AddUpdateNS(snippet string) { 95 spec.updateNS.Put(snippet) 96 } 97 98 // EmitUpdateNSFunc returns a function for emitting update-ns snippets. 99 func (spec *Specification) EmitUpdateNSFunc() func(f string, args ...interface{}) { 100 return func(f string, args ...interface{}) { 101 spec.AddUpdateNS(fmt.Sprintf(f, args...)) 102 } 103 } 104 105 // UpdateNSIndexOf returns the index of a previously added snippet. 106 func (spec *Specification) UpdateNSIndexOf(snippet string) (idx int, ok bool) { 107 return spec.updateNS.IndexOf(snippet) 108 } 109 110 // AddLayout adds apparmor snippets based on the layout of the snap. 111 // 112 // The per-snap snap-update-ns profiles are composed via a template and 113 // snippets for the snap. The snippets may allow (depending on the snippet): 114 // - mount profiles via the content interface 115 // - creating missing mount point directories under $SNAP* (the 'tree' 116 // of permissions is needed for SecureMkDirAll that uses 117 // open(..., O_NOFOLLOW) and mkdirat() using the resulting file descriptor) 118 // - creating a placeholder directory in /tmp/.snap/ in the per-snap mount 119 // namespace to support writable mimic which uses tmpfs and bind mount to 120 // poke holes in arbitrary read-only locations 121 // - mounting/unmounting any part of $SNAP into placeholder directory 122 // - mounting/unmounting tmpfs over the original $SNAP/** location 123 // - mounting/unmounting from placeholder back to $SNAP/** (for reconstructing 124 // the data) 125 // Importantly, the above mount operations are happening within the per-snap 126 // mount namespace. 127 func (spec *Specification) AddLayout(si *snap.Info) { 128 if len(si.Layout) == 0 { 129 return 130 } 131 132 // Walk the layout elements in deterministic order, by mount point name. 133 paths := make([]string, 0, len(si.Layout)) 134 for path := range si.Layout { 135 paths = append(paths, path) 136 } 137 sort.Strings(paths) 138 139 // Get tags describing all apps and hooks. 140 tags := make([]string, 0, len(si.Apps)+len(si.Hooks)) 141 for _, app := range si.Apps { 142 tags = append(tags, app.SecurityTag()) 143 } 144 for _, hook := range si.Hooks { 145 tags = append(tags, hook.SecurityTag()) 146 } 147 148 // Append layout snippets to all tags; the layout applies equally to the 149 // entire snap as the entire snap uses one mount namespace. 150 if spec.snippets == nil { 151 spec.snippets = make(map[string][]string) 152 } 153 for _, tag := range tags { 154 for _, path := range paths { 155 snippet := snippetFromLayout(si.Layout[path]) 156 spec.snippets[tag] = append(spec.snippets[tag], snippet) 157 } 158 sort.Strings(spec.snippets[tag]) 159 } 160 161 emit := spec.EmitUpdateNSFunc() 162 163 // Append update-ns snippets that allow constructing the layout. 164 for _, path := range paths { 165 l := si.Layout[path] 166 emit(" # Layout %s\n", l.String()) 167 path := si.ExpandSnapVariables(l.Path) 168 switch { 169 case l.Bind != "": 170 bind := si.ExpandSnapVariables(l.Bind) 171 // Allow bind mounting the layout element. 172 emit(" mount options=(rbind, rw) %s/ -> %s/,\n", bind, path) 173 emit(" mount options=(rprivate) -> %s/,\n", path) 174 emit(" umount %s/,\n", path) 175 // Allow constructing writable mimic in both bind-mount source and mount point. 176 GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory 177 GenWritableProfile(emit, bind, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION 178 case l.BindFile != "": 179 bindFile := si.ExpandSnapVariables(l.BindFile) 180 // Allow bind mounting the layout element. 181 emit(" mount options=(bind, rw) %s -> %s,\n", bindFile, path) 182 emit(" mount options=(rprivate) -> %s,\n", path) 183 emit(" umount %s,\n", path) 184 // Allow constructing writable mimic in both bind-mount source and mount point. 185 GenWritableFileProfile(emit, path, 2) // At least / and /some-top-level-directory 186 GenWritableFileProfile(emit, bindFile, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION 187 case l.Type == "tmpfs": 188 emit(" mount fstype=tmpfs tmpfs -> %s/,\n", path) 189 emit(" mount options=(rprivate) -> %s/,\n", path) 190 emit(" umount %s/,\n", path) 191 // Allow constructing writable mimic to mount point. 192 GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory 193 case l.Symlink != "": 194 // Allow constructing writable mimic to symlink parent directory. 195 emit(" %s rw,\n", path) 196 GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory 197 } 198 } 199 } 200 201 // AddOvername adds AppArmor snippets allowing remapping of snap 202 // directories for parallel installed snaps 203 // 204 // Specifically snap-update-ns will apply the following bind mounts 205 // - /snap/foo_bar -> /snap/foo 206 // - /var/snap/foo_bar -> /var/snap/foo 207 // - /home/joe/snap/foo_bar -> /home/joe/snap/foo 208 func (spec *Specification) AddOvername(si *snap.Info) { 209 if si.InstanceKey == "" { 210 return 211 } 212 var buf bytes.Buffer 213 214 // /snap/foo_bar -> /snap/foo 215 fmt.Fprintf(&buf, " # Allow parallel instance snap mount namespace adjustments\n") 216 fmt.Fprintf(&buf, " mount options=(rw rbind) /snap/%s/ -> /snap/%s/,\n", si.InstanceName(), si.SnapName()) 217 // /var/snap/foo_bar -> /var/snap/foo 218 fmt.Fprintf(&buf, " mount options=(rw rbind) /var/snap/%s/ -> /var/snap/%s/,\n", si.InstanceName(), si.SnapName()) 219 spec.AddUpdateNS(buf.String()) 220 } 221 222 // isProbably writable returns true if the path is probably representing writable area. 223 func isProbablyWritable(path string) bool { 224 return strings.HasPrefix(path, "/var/snap/") || strings.HasPrefix(path, "/home/") || strings.HasPrefix(path, "/root/") 225 } 226 227 // isProbablyPresent returns true if the path is probably already present. 228 // 229 // This is used as a simple hint to not inject writable path rules for things 230 // that we don't expect to create as they are already present in the skeleton 231 // file-system tree. 232 func isProbablyPresent(path string) bool { 233 return path == "/" || path == "/snap" || path == "/var" || path == "/var/snap" || path == "/tmp" || path == "/usr" || path == "/etc" 234 } 235 236 // GenWritableMimicProfile generates apparmor rules for a writable mimic at the given path. 237 func GenWritableMimicProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) { 238 emit(" # Writable mimic %s\n", path) 239 240 iter, err := strutil.NewPathIterator(path) 241 if err != nil { 242 panic(err) 243 } 244 245 // Handle the prefix that is assumed to exist first. 246 emit(" # .. permissions for traversing the prefix that is assumed to exist\n") 247 for iter.Next() { 248 if iter.Depth() < assumedPrefixDepth { 249 emit(" %s r,\n", iter.CurrentPath()) 250 } 251 } 252 253 // Rewind the iterator and handle the part that needs to be created. 254 iter.Rewind() 255 for iter.Next() { 256 if iter.Depth() < assumedPrefixDepth { 257 continue 258 } 259 // Assume that the mimic needs to be created at the given prefix of the 260 // full mimic path. This is called a mimic "variant". Both of the paths 261 // must end with a slash as this is important for apparmor file vs 262 // directory path semantics. 263 mimicPath := filepath.Join(iter.CurrentBase(), iter.CurrentCleanName()) + "/" 264 mimicAuxPath := filepath.Join("/tmp/.snap", iter.CurrentPath()) + "/" 265 emit(" # .. variant with mimic at %s\n", mimicPath) 266 emit(" # Allow reading the mimic directory, it must exist in the first place.\n") 267 emit(" %s r,\n", mimicPath) 268 emit(" # Allow setting the read-only directory aside via a bind mount.\n") 269 emit(" %s rw,\n", mimicAuxPath) 270 emit(" mount options=(rbind, rw) %s -> %s,\n", mimicPath, mimicAuxPath) 271 emit(" # Allow mounting tmpfs over the read-only directory.\n") 272 emit(" mount fstype=tmpfs options=(rw) tmpfs -> %s,\n", mimicPath) 273 emit(" # Allow creating empty files and directories for bind mounting things\n" + 274 " # to reconstruct the now-writable parent directory.\n") 275 emit(" %s*/ rw,\n", mimicAuxPath) 276 emit(" %s*/ rw,\n", mimicPath) 277 emit(" mount options=(rbind, rw) %s*/ -> %s*/,\n", mimicAuxPath, mimicPath) 278 emit(" %s* rw,\n", mimicAuxPath) 279 emit(" %s* rw,\n", mimicPath) 280 emit(" mount options=(bind, rw) %s* -> %s*,\n", mimicAuxPath, mimicPath) 281 emit(" # Allow unmounting the auxiliary directory.\n" + 282 " # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)\n") 283 emit(" mount options=(rprivate) -> %s,\n", mimicAuxPath) 284 emit(" umount %s,\n", mimicAuxPath) 285 emit(" # Allow unmounting the destination directory as well as anything\n" + 286 " # inside. This lets us perform the undo plan in case the writable\n" + 287 " # mimic fails.\n") 288 emit(" mount options=(rprivate) -> %s,\n", mimicPath) 289 emit(" mount options=(rprivate) -> %s*,\n", mimicPath) 290 emit(" mount options=(rprivate) -> %s*/,\n", mimicPath) 291 emit(" umount %s,\n", mimicPath) 292 emit(" umount %s*,\n", mimicPath) 293 emit(" umount %s*/,\n", mimicPath) 294 } 295 } 296 297 // GenWritableFileProfile writes a profile for snap-update-ns for making given file writable. 298 func GenWritableFileProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) { 299 if path == "/" { 300 return 301 } 302 if isProbablyWritable(path) { 303 emit(" # Writable file %s\n", path) 304 emit(" %s rw,\n", path) 305 for p := parent(path); !isProbablyPresent(p); p = parent(p) { 306 emit(" %s/ rw,\n", p) 307 } 308 } else { 309 parentPath := parent(path) 310 GenWritableMimicProfile(emit, parentPath, assumedPrefixDepth) 311 } 312 } 313 314 // GenWritableProfile generates a profile for snap-update-ns for making given directory writable. 315 func GenWritableProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) { 316 if path == "/" { 317 return 318 } 319 if isProbablyWritable(path) { 320 emit(" # Writable directory %s\n", path) 321 for p := path; !isProbablyPresent(p); p = parent(p) { 322 emit(" %s/ rw,\n", p) 323 } 324 } else { 325 parentPath := parent(path) 326 GenWritableMimicProfile(emit, parentPath, assumedPrefixDepth) 327 } 328 } 329 330 // parent returns the parent directory of a given path. 331 func parent(path string) string { 332 result, _ := filepath.Split(path) 333 result = filepath.Clean(result) 334 return result 335 } 336 337 // Snippets returns a deep copy of all the added application snippets. 338 func (spec *Specification) Snippets() map[string][]string { 339 return copySnippets(spec.snippets) 340 } 341 342 // SnippetForTag returns a combined snippet for given security tag with individual snippets 343 // joined with newline character. Empty string is returned for non-existing security tag. 344 func (spec *Specification) SnippetForTag(tag string) string { 345 return strings.Join(spec.snippets[tag], "\n") 346 } 347 348 // SecurityTags returns a list of security tags which have a snippet. 349 func (spec *Specification) SecurityTags() []string { 350 var tags []string 351 for t := range spec.snippets { 352 tags = append(tags, t) 353 } 354 sort.Strings(tags) 355 return tags 356 } 357 358 // UpdateNS returns a deep copy of all the added snap-update-ns snippets. 359 func (spec *Specification) UpdateNS() []string { 360 return spec.updateNS.Items() 361 } 362 363 func snippetFromLayout(layout *snap.Layout) string { 364 mountPoint := layout.Snap.ExpandSnapVariables(layout.Path) 365 if layout.Bind != "" || layout.Type == "tmpfs" { 366 return fmt.Sprintf("# Layout path: %s\n%s{,/**} mrwklix,", mountPoint, mountPoint) 367 } else if layout.BindFile != "" { 368 return fmt.Sprintf("# Layout path: %s\n%s mrwklix,", mountPoint, mountPoint) 369 } 370 return fmt.Sprintf("# Layout path: %s\n# (no extra permissions required for symlink)", mountPoint) 371 } 372 373 func copySnippets(m map[string][]string) map[string][]string { 374 result := make(map[string][]string, len(m)) 375 for k, v := range m { 376 result[k] = append([]string(nil), v...) 377 } 378 return result 379 } 380 381 // Implementation of methods required by interfaces.Specification 382 383 // AddConnectedPlug records apparmor-specific side-effects of having a connected plug. 384 func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 385 type definer interface { 386 AppArmorConnectedPlug(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error 387 } 388 if iface, ok := iface.(definer); ok { 389 restore := spec.setScope(plug.SecurityTags()) 390 defer restore() 391 return iface.AppArmorConnectedPlug(spec, plug, slot) 392 } 393 return nil 394 } 395 396 // AddConnectedSlot records mount-specific side-effects of having a connected slot. 397 func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 398 type definer interface { 399 AppArmorConnectedSlot(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error 400 } 401 if iface, ok := iface.(definer); ok { 402 restore := spec.setScope(slot.SecurityTags()) 403 defer restore() 404 return iface.AppArmorConnectedSlot(spec, plug, slot) 405 } 406 return nil 407 } 408 409 // AddPermanentPlug records mount-specific side-effects of having a plug. 410 func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *snap.PlugInfo) error { 411 type definer interface { 412 AppArmorPermanentPlug(spec *Specification, plug *snap.PlugInfo) error 413 } 414 if iface, ok := iface.(definer); ok { 415 restore := spec.setScope(plug.SecurityTags()) 416 defer restore() 417 return iface.AppArmorPermanentPlug(spec, plug) 418 } 419 return nil 420 } 421 422 // AddPermanentSlot records mount-specific side-effects of having a slot. 423 func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *snap.SlotInfo) error { 424 type definer interface { 425 AppArmorPermanentSlot(spec *Specification, slot *snap.SlotInfo) error 426 } 427 if iface, ok := iface.(definer); ok { 428 restore := spec.setScope(slot.SecurityTags()) 429 defer restore() 430 return iface.AppArmorPermanentSlot(spec, slot) 431 } 432 return nil 433 } 434 435 // SetUsesPtraceTrace records when to omit explicit ptrace deny rules 436 func (spec *Specification) SetUsesPtraceTrace() { 437 spec.usesPtraceTrace = true 438 } 439 440 func (spec *Specification) UsesPtraceTrace() bool { 441 return spec.usesPtraceTrace 442 } 443 444 // SetSuppressPtraceTrace to request explicit ptrace deny rules 445 func (spec *Specification) SetSuppressPtraceTrace() { 446 spec.suppressPtraceTrace = true 447 } 448 449 func (spec *Specification) SuppressPtraceTrace() bool { 450 return spec.suppressPtraceTrace 451 } 452 453 // SetSuppressHomeIx to request explicit ptrace deny rules 454 func (spec *Specification) SetSuppressHomeIx() { 455 spec.suppressHomeIx = true 456 } 457 458 func (spec *Specification) SuppressHomeIx() bool { 459 return spec.suppressHomeIx 460 }