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  }