github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap-update-ns/change.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 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 main 21 22 import ( 23 "errors" 24 "fmt" 25 "os" 26 "path/filepath" 27 "sort" 28 "strings" 29 "syscall" 30 31 "github.com/snapcore/snapd/features" 32 "github.com/snapcore/snapd/logger" 33 "github.com/snapcore/snapd/osutil" 34 ) 35 36 // Action represents a mount action (mount, remount, unmount, etc). 37 type Action string 38 39 const ( 40 // Keep indicates that a given mount entry should be kept as-is. 41 Keep Action = "keep" 42 // Mount represents an action that results in mounting something somewhere. 43 Mount Action = "mount" 44 // Unmount represents an action that results in unmounting something from somewhere. 45 Unmount Action = "unmount" 46 // Remount when needed 47 ) 48 49 var ( 50 // ErrIgnoredMissingMount is returned when a mount entry has 51 // been marked with x-snapd.ignore-missing, and the mount 52 // source or target do not exist. 53 ErrIgnoredMissingMount = errors.New("mount source or target are missing") 54 ) 55 56 // Change describes a change to the mount table (action and the entry to act on). 57 type Change struct { 58 Entry osutil.MountEntry 59 Action Action 60 } 61 62 // String formats mount change to a human-readable line. 63 func (c Change) String() string { 64 return fmt.Sprintf("%s (%s)", c.Action, c.Entry) 65 } 66 67 // changePerform is Change.Perform that can be mocked for testing. 68 var changePerform func(*Change, *Assumptions) ([]*Change, error) 69 70 // mimicRequired provides information if an error warrants a writable mimic. 71 // 72 // The returned path is the location where a mimic should be constructed. 73 func mimicRequired(err error) (needsMimic bool, path string) { 74 switch err.(type) { 75 case *ReadOnlyFsError: 76 rofsErr := err.(*ReadOnlyFsError) 77 return true, rofsErr.Path 78 case *TrespassingError: 79 tErr := err.(*TrespassingError) 80 return true, tErr.ViolatedPath 81 } 82 return false, "" 83 } 84 85 func (c *Change) createPath(path string, pokeHoles bool, as *Assumptions) ([]*Change, error) { 86 // If we've been asked to create a missing path, and the mount 87 // entry uses the ignore-missing option, return an error. 88 if c.Entry.XSnapdIgnoreMissing() { 89 return nil, ErrIgnoredMissingMount 90 } 91 92 var err error 93 var changes []*Change 94 95 // In case we need to create something, some constants. 96 const ( 97 mode = 0755 98 uid = 0 99 gid = 0 100 ) 101 102 // If the element doesn't exist we can attempt to create it. We will 103 // create the parent directory and then the final element relative to it. 104 // The traversed space may be writable so we just try to create things 105 // first. 106 kind := c.Entry.XSnapdKind() 107 108 // TODO: re-factor this, if possible, with inspection and preemptive 109 // creation after the current release ships. This should be possible but 110 // will affect tests heavily (churn, not safe before release). 111 rs := as.RestrictionsFor(path) 112 switch kind { 113 case "": 114 err = MkdirAll(path, mode, uid, gid, rs) 115 case "file": 116 err = MkfileAll(path, mode, uid, gid, rs) 117 case "symlink": 118 err = MksymlinkAll(path, mode, uid, gid, c.Entry.XSnapdSymlink(), rs) 119 } 120 if needsMimic, mimicPath := mimicRequired(err); needsMimic && pokeHoles { 121 // If the error can be recovered by using a writable mimic 122 // then construct one and try again. 123 changes, err = createWritableMimic(mimicPath, path, as) 124 if err != nil { 125 err = fmt.Errorf("cannot create writable mimic over %q: %s", mimicPath, err) 126 } else { 127 // Try once again. Note that we care *just* about the error. We have already 128 // performed the hole poking and thus additional changes must be nil. 129 _, err = c.createPath(path, false, as) 130 } 131 } 132 return changes, err 133 } 134 135 func (c *Change) ensureTarget(as *Assumptions) ([]*Change, error) { 136 var changes []*Change 137 138 kind := c.Entry.XSnapdKind() 139 path := c.Entry.Dir 140 141 // We use lstat to ensure that we don't follow a symlink in case one was 142 // set up by the snap. Note that at the time this is run, all the snap's 143 // processes are frozen but if the path is a directory controlled by the 144 // user (typically in /home) then we may still race with user processes 145 // that change it. 146 fi, err := osLstat(path) 147 148 if err == nil { 149 // If the element already exists we just need to ensure it is of 150 // the correct type. The desired type depends on the kind of entry 151 // we are working with. 152 switch kind { 153 case "": 154 if !fi.Mode().IsDir() { 155 err = fmt.Errorf("cannot use %q as mount point: not a directory", path) 156 } 157 case "file": 158 if !fi.Mode().IsRegular() { 159 err = fmt.Errorf("cannot use %q as mount point: not a regular file", path) 160 } 161 case "symlink": 162 if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 163 // Create path verifies the symlink or fails if it is not what we wanted. 164 _, err = c.createPath(path, false, as) 165 } else { 166 err = fmt.Errorf("cannot create symlink in %q: existing file in the way", path) 167 } 168 } 169 } else if os.IsNotExist(err) { 170 changes, err = c.createPath(path, true, as) 171 } else { 172 // If we cannot inspect the element let's just bail out. 173 err = fmt.Errorf("cannot inspect %q: %v", path, err) 174 } 175 return changes, err 176 } 177 178 func (c *Change) ensureSource(as *Assumptions) ([]*Change, error) { 179 var changes []*Change 180 181 // We only have to do ensure bind mount source exists. 182 // This also rules out symlinks. 183 flags, _ := osutil.MountOptsToCommonFlags(c.Entry.Options) 184 if flags&syscall.MS_BIND == 0 { 185 return nil, nil 186 } 187 188 kind := c.Entry.XSnapdKind() 189 path := c.Entry.Name 190 fi, err := osLstat(path) 191 192 if err == nil { 193 // If the element already exists we just need to ensure it is of 194 // the correct type. The desired type depends on the kind of entry 195 // we are working with. 196 switch kind { 197 case "": 198 if !fi.Mode().IsDir() { 199 err = fmt.Errorf("cannot use %q as bind-mount source: not a directory", path) 200 } 201 case "file": 202 if !fi.Mode().IsRegular() { 203 err = fmt.Errorf("cannot use %q as bind-mount source: not a regular file", path) 204 } 205 } 206 } else if os.IsNotExist(err) { 207 // NOTE: This createPath is using pokeHoles, to make read-only places 208 // writable, but only for layouts and not for other (typically content 209 // sharing) mount entries. 210 // 211 // This is done because the changes made with pokeHoles=true are only 212 // visible in this current mount namespace and are not generally 213 // visible from other snaps because they inhabit different namespaces. 214 // 215 // In other words, changes made here are only observable by the single 216 // snap they apply to. As such they are useless for content sharing but 217 // very much useful to layouts. 218 pokeHoles := c.Entry.XSnapdOrigin() == "layout" 219 changes, err = c.createPath(path, pokeHoles, as) 220 } else { 221 // If we cannot inspect the element let's just bail out. 222 err = fmt.Errorf("cannot inspect %q: %v", path, err) 223 } 224 225 return changes, err 226 } 227 228 // changePerformImpl is the real implementation of Change.Perform 229 func changePerformImpl(c *Change, as *Assumptions) (changes []*Change, err error) { 230 if c.Action == Mount { 231 var changesSource, changesTarget []*Change 232 // We may be asked to bind mount a file, bind mount a directory, mount 233 // a filesystem over a directory, or create a symlink (which is abusing 234 // the "mount" concept slightly). That actual operation is performed in 235 // c.lowLevelPerform. Here we just set the stage to make that possible. 236 // 237 // As a result of this ensure call we may need to make the medium writable 238 // and that's why we may return more changes as a result of performing this 239 // one. 240 changesTarget, err = c.ensureTarget(as) 241 // NOTE: we are collecting changes even if things fail. This is so that 242 // upper layers can perform undo correctly. 243 changes = append(changes, changesTarget...) 244 if err != nil { 245 return changes, err 246 } 247 248 // At this time we can be sure that the target element (for files and 249 // directories) exists and is of the right type or that it (for 250 // symlinks) doesn't exist but the parent directory does. 251 // This property holds as long as we don't interact with locations that 252 // are under the control of regular (non-snap) processes that are not 253 // suspended and may be racing with us. 254 changesSource, err = c.ensureSource(as) 255 // NOTE: we are collecting changes even if things fail. This is so that 256 // upper layers can perform undo correctly. 257 changes = append(changes, changesSource...) 258 if err != nil { 259 return changes, err 260 } 261 } 262 263 // Perform the underlying mount / unmount / unlink call. 264 err = c.lowLevelPerform(as) 265 return changes, err 266 } 267 268 func init() { 269 changePerform = changePerformImpl 270 } 271 272 // Perform executes the desired mount or unmount change using system calls. 273 // Filesystems that depend on helper programs or multiple independent calls to 274 // the kernel (--make-shared, for example) are unsupported. 275 // 276 // Perform may synthesize *additional* changes that were necessary to perform 277 // this change (such as mounted tmpfs or overlayfs). 278 func (c *Change) Perform(as *Assumptions) ([]*Change, error) { 279 return changePerform(c, as) 280 } 281 282 // lowLevelPerform is simple bridge from Change to mount / unmount syscall. 283 func (c *Change) lowLevelPerform(as *Assumptions) error { 284 var err error 285 switch c.Action { 286 case Mount: 287 kind := c.Entry.XSnapdKind() 288 switch kind { 289 case "symlink": 290 // symlinks are handled in createInode directly, nothing to do here. 291 case "", "file": 292 flags, unparsed := osutil.MountOptsToCommonFlags(c.Entry.Options) 293 // Split the mount flags from the event propagation changes. 294 // Those have to be applied separately. 295 const propagationMask = syscall.MS_SHARED | syscall.MS_SLAVE | syscall.MS_PRIVATE | syscall.MS_UNBINDABLE 296 maskedFlagsRecursive := flags & syscall.MS_REC 297 maskedFlagsPropagation := flags & propagationMask 298 maskedFlagsNotPropagationNotRecursive := flags & ^(propagationMask | syscall.MS_REC) 299 300 var flagsForMount uintptr 301 if flags&syscall.MS_BIND == syscall.MS_BIND { 302 // bind / rbind mount 303 flagsForMount = uintptr(maskedFlagsNotPropagationNotRecursive | maskedFlagsRecursive) 304 err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flagsForMount)) 305 } else { 306 // normal mount, not bind / rbind, not propagation change 307 flagsForMount = uintptr(maskedFlagsNotPropagationNotRecursive) 308 err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flagsForMount), strings.Join(unparsed, ",")) 309 } 310 logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, flagsForMount, strings.Join(unparsed, ","), err) 311 if err == nil && maskedFlagsPropagation != 0 { 312 // now change mount propagation (shared/rshared, private/rprivate, 313 // slave/rslave, unbindable/runbindable). 314 flagsForMount := uintptr(maskedFlagsPropagation | maskedFlagsRecursive) 315 err = sysMount("none", c.Entry.Dir, "", flagsForMount, "") 316 logger.Debugf("mount %q %q %q %d %q (error: %v)", "none", c.Entry.Dir, "", flagsForMount, "", err) 317 } 318 if err == nil { 319 as.AddChange(c) 320 } 321 } 322 return err 323 case Unmount: 324 kind := c.Entry.XSnapdKind() 325 switch kind { 326 case "symlink": 327 err = osRemove(c.Entry.Dir) 328 logger.Debugf("remove %q (error: %v)", c.Entry.Dir, err) 329 case "", "file": 330 // Detach the mount point instead of unmounting it if requested. 331 flags := umountNoFollow 332 if c.Entry.XSnapdDetach() { 333 flags |= syscall.MNT_DETACH 334 // If we are detaching something then before performing the actual detach 335 // switch the entire hierarchy to private event propagation (that is, 336 // none). This works around a bit of peculiar kernel behavior when the 337 // kernel reports EBUSY during a detach operation, because the changes 338 // propagate in a way that conflicts with itself. This is also documented 339 // in umount(2). 340 err = sysMount("none", c.Entry.Dir, "", syscall.MS_REC|syscall.MS_PRIVATE, "") 341 logger.Debugf("mount --make-rprivate %q (error: %v)", c.Entry.Dir, err) 342 } 343 344 // Perform the raw unmount operation. 345 if err == nil { 346 err = sysUnmount(c.Entry.Dir, flags) 347 } 348 if err == nil { 349 as.AddChange(c) 350 } 351 logger.Debugf("umount %q (error: %v)", c.Entry.Dir, err) 352 if err != nil { 353 return err 354 } 355 356 // Open a path of the file we are considering the removal of. 357 path := c.Entry.Dir 358 var fd int 359 fd, err = OpenPath(path) 360 if err != nil { 361 return err 362 } 363 defer sysClose(fd) 364 365 // Don't attempt to remove anything from squashfs. 366 var statfsBuf syscall.Statfs_t 367 err = sysFstatfs(fd, &statfsBuf) 368 if err != nil { 369 return err 370 } 371 if statfsBuf.Type == SquashfsMagic { 372 return nil 373 } 374 375 if kind == "file" { 376 // Don't attempt to remove non-empty files since they cannot be 377 // the placeholders we created. 378 var statBuf syscall.Stat_t 379 err = sysFstat(fd, &statBuf) 380 if err != nil { 381 return err 382 } 383 if statBuf.Size != 0 { 384 return nil 385 } 386 } 387 388 // Remove the file or directory while using the full path. There's 389 // no way to avoid a race here since there's no way to unlink a 390 // file solely by file descriptor. 391 err = osRemove(path) 392 // Unpack the low-level error that osRemove wraps into PathError. 393 if packed, ok := err.(*os.PathError); ok { 394 err = packed.Err 395 } 396 // If we were removing a directory but it was not empty then just 397 // ignore the error. This is the equivalent of the non-empty file 398 // check we do above. See rmdir(2) for explanation why we accept 399 // more than one errno value. 400 if kind == "" && (err == syscall.ENOTEMPTY || err == syscall.EEXIST) { 401 return nil 402 } 403 if features.RobustMountNamespaceUpdates.IsEnabled() { 404 // FIXME: This should not be necessary. It is necessary because 405 // mimic construction code is not considering all layouts in tandem 406 // and doesn't know enough about base file system to construct 407 // mimics in the order that would prevent them from nesting. 408 // 409 // By ignoring EBUSY here and by continuing to tear down the mimic 410 // tmpfs entirely (without any reuse) we guarantee that at the end 411 // of the day the nested mimic case is entirely removed. 412 // 413 // In an ideal world we would model this better and could do 414 // without this edge case. 415 if kind == "" && err == syscall.EBUSY { 416 logger.Debugf("cannot remove busy mount point %q", path) 417 return nil 418 } 419 } 420 } 421 return err 422 case Keep: 423 as.AddChange(c) 424 return nil 425 } 426 return fmt.Errorf("cannot process mount change: unknown action: %q", c.Action) 427 } 428 429 // neededChangesOld is the real implementation of NeededChanges 430 // This function is used when RobustMountNamespaceUpdate is not enabled. 431 func neededChangesOld(currentProfile, desiredProfile *osutil.MountProfile) []*Change { 432 // Copy both profiles as we will want to mutate them. 433 current := make([]osutil.MountEntry, len(currentProfile.Entries)) 434 copy(current, currentProfile.Entries) 435 desired := make([]osutil.MountEntry, len(desiredProfile.Entries)) 436 copy(desired, desiredProfile.Entries) 437 438 // Clean the directory part of both profiles. This is done so that we can 439 // easily test if a given directory is a subdirectory with 440 // strings.HasPrefix coupled with an extra slash character. 441 for i := range current { 442 current[i].Dir = filepath.Clean(current[i].Dir) 443 } 444 for i := range desired { 445 desired[i].Dir = filepath.Clean(desired[i].Dir) 446 } 447 448 // Sort both lists by directory name with implicit trailing slash. 449 sort.Sort(byOriginAndMagicDir(current)) 450 sort.Sort(byOriginAndMagicDir(desired)) 451 452 // Construct a desired directory map. 453 desiredMap := make(map[string]*osutil.MountEntry) 454 for i := range desired { 455 desiredMap[desired[i].Dir] = &desired[i] 456 } 457 458 // Indexed by mount point path. 459 reuse := make(map[string]bool) 460 // Indexed by entry ID 461 desiredIDs := make(map[string]bool) 462 var skipDir string 463 464 // Collect the IDs of desired changes. 465 // We need that below to keep implicit changes from the current profile. 466 for i := range desired { 467 desiredIDs[desired[i].XSnapdEntryID()] = true 468 } 469 470 // Compute reusable entries: those which are equal in current and desired and which 471 // are not prefixed by another entry that changed. 472 for i := range current { 473 dir := current[i].Dir 474 if skipDir != "" && strings.HasPrefix(dir, skipDir) { 475 logger.Debugf("skipping entry %q", current[i]) 476 continue 477 } 478 skipDir = "" // reset skip prefix as it no longer applies 479 480 // Reuse synthetic entries if their needed-by entry is desired. 481 // Synthetic entries cannot exist on their own and always couple to a 482 // non-synthetic entry. 483 484 // NOTE: Synthetic changes have a special purpose. 485 // 486 // They are a "shadow" of mount events that occurred to allow one of 487 // the desired mount entries to be possible. The changes have only one 488 // goal: tell snap-update-ns how those mount events can be undone in 489 // case they are no longer needed. The actual changes may have been 490 // different and may have involved steps not represented as synthetic 491 // mount entires as long as those synthetic entries can be undone to 492 // reverse the effect. In reality each non-tmpfs synthetic entry was 493 // constructed using a temporary bind mount that contained the original 494 // mount entries of a directory that was hidden with a tmpfs, but this 495 // fact was lost. 496 if current[i].XSnapdSynthetic() && desiredIDs[current[i].XSnapdNeededBy()] { 497 logger.Debugf("reusing synthetic entry %q", current[i]) 498 reuse[dir] = true 499 continue 500 } 501 502 // Reuse entries that are desired and identical in the current profile. 503 if entry, ok := desiredMap[dir]; ok && current[i].Equal(entry) { 504 logger.Debugf("reusing unchanged entry %q", current[i]) 505 reuse[dir] = true 506 continue 507 } 508 509 skipDir = strings.TrimSuffix(dir, "/") + "/" 510 } 511 512 logger.Debugf("desiredIDs: %v", desiredIDs) 513 logger.Debugf("reuse: %v", reuse) 514 515 // We are now ready to compute the necessary mount changes. 516 var changes []*Change 517 518 // Unmount entries not reused in reverse to handle children before their parent. 519 for i := len(current) - 1; i >= 0; i-- { 520 if reuse[current[i].Dir] { 521 changes = append(changes, &Change{Action: Keep, Entry: current[i]}) 522 } else { 523 var entry osutil.MountEntry = current[i] 524 entry.Options = append([]string(nil), entry.Options...) 525 // If the mount entry can potentially host nested mount points then detach 526 // rather than unmount, since detach will always succeed. 527 shouldDetach := entry.Type == "tmpfs" || entry.OptBool("bind") || entry.OptBool("rbind") 528 if shouldDetach && !entry.XSnapdDetach() { 529 entry.Options = append(entry.Options, osutil.XSnapdDetach()) 530 } 531 changes = append(changes, &Change{Action: Unmount, Entry: entry}) 532 } 533 } 534 535 // Mount desired entries not reused. 536 for i := range desired { 537 if !reuse[desired[i].Dir] { 538 changes = append(changes, &Change{Action: Mount, Entry: desired[i]}) 539 } 540 } 541 542 return changes 543 } 544 545 // neededChangesNew is the real implementation of NeededChanges 546 // This function is used when RobustMountNamespaceUpdate is enabled. 547 func neededChangesNew(currentProfile, desiredProfile *osutil.MountProfile) []*Change { 548 // Copy both profiles as we will want to mutate them. 549 current := make([]osutil.MountEntry, len(currentProfile.Entries)) 550 copy(current, currentProfile.Entries) 551 desired := make([]osutil.MountEntry, len(desiredProfile.Entries)) 552 copy(desired, desiredProfile.Entries) 553 554 // Clean the directory part of both profiles. This is done so that we can 555 // easily test if a given directory is a subdirectory with 556 // strings.HasPrefix coupled with an extra slash character. 557 for i := range current { 558 current[i].Dir = filepath.Clean(current[i].Dir) 559 } 560 for i := range desired { 561 desired[i].Dir = filepath.Clean(desired[i].Dir) 562 } 563 564 // Sort both lists by directory name with implicit trailing slash. 565 sort.Sort(byOriginAndMagicDir(current)) 566 sort.Sort(byOriginAndMagicDir(desired)) 567 568 // We are now ready to compute the necessary mount changes. 569 var changes []*Change 570 571 // Unmount entries in reverse order, so that the most nested element is 572 // always processed first. 573 for i := len(current) - 1; i >= 0; i-- { 574 var entry osutil.MountEntry = current[i] 575 entry.Options = append([]string(nil), entry.Options...) 576 switch { 577 case entry.XSnapdSynthetic() && entry.Type == "tmpfs": 578 // Synthetic changes are rooted under a tmpfs, detach that tmpfs to 579 // remove them all. 580 if !entry.XSnapdDetach() { 581 entry.Options = append(entry.Options, osutil.XSnapdDetach()) 582 } 583 case entry.XSnapdSynthetic(): 584 // Consume all other syn ethic entries without emitting either a 585 // mount, unmount or keep change. This relies on the fact that all 586 // synthetic mounts are created by a mimic underneath a tmpfs that 587 // is detached, as coded above. 588 continue 589 case entry.OptBool("rbind") || entry.Type == "tmpfs": 590 // Recursive bind mounts and non-mimic tmpfs mounts need to be 591 // detached because they can contain other mount points that can 592 // otherwise propagate in a self-conflicting way. 593 if !entry.XSnapdDetach() { 594 entry.Options = append(entry.Options, osutil.XSnapdDetach()) 595 } 596 } 597 // Unmount all changes that were not eliminated. 598 changes = append(changes, &Change{Action: Unmount, Entry: entry}) 599 } 600 601 // Mount desired entries. 602 for i := range desired { 603 changes = append(changes, &Change{Action: Mount, Entry: desired[i]}) 604 } 605 606 return changes 607 } 608 609 // NeededChanges computes the changes required to change current to desired mount entries. 610 // 611 // The algorithm differs depending on the value of the robust mount namespace 612 // updates feature flag. If the flag is enabled then the current profile is 613 // entirely undone and the desired profile is constructed from scratch. 614 // 615 // If the flag is disabled then a diff-like operation on the mount profile is 616 // computed. Some of the mount entries from the current profile may be reused. 617 // The diff approach doesn't function correctly in cases of nested mimics. 618 var NeededChanges = func(current, desired *osutil.MountProfile) []*Change { 619 if features.RobustMountNamespaceUpdates.IsEnabled() { 620 return neededChangesNew(current, desired) 621 } 622 return neededChangesOld(current, desired) 623 }