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