github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap-update-ns/trespassing.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 main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"syscall"
    28  
    29  	"github.com/snapcore/snapd/logger"
    30  )
    31  
    32  // Assumptions track the assumptions about the state of the filesystem.
    33  //
    34  // Assumptions constitute the global part of the write restriction management.
    35  // Assumptions are global in the sense that they span multiple distinct write
    36  // operations. In contrast, Restrictions track per-operation state.
    37  type Assumptions struct {
    38  	unrestrictedPaths []string
    39  	pastChanges       []*Change
    40  
    41  	// verifiedDevices represents the set of devices that are verified as a tmpfs
    42  	// that was mounted by snapd. Those are only discovered on-demand. The
    43  	// major:minor number is packed into one uint64 as in syscall.Stat_t.Dev
    44  	// field.
    45  	verifiedDevices map[uint64]bool
    46  
    47  	// modeHints overrides implicit 0755 mode of directories created while
    48  	// ensuring source and target paths exist.
    49  	modeHints []ModeHint
    50  }
    51  
    52  // ModeHint provides mode for directories created to satisfy mount changes.
    53  type ModeHint struct {
    54  	PathGlob string
    55  	Mode     os.FileMode
    56  }
    57  
    58  // AddUnrestrictedPaths adds a list of directories where writing is allowed
    59  // even if it would hit the real host filesystem (or transit through the host
    60  // filesystem). This is intended to be used with certain well-known locations
    61  // such as /tmp, $SNAP_DATA and $SNAP.
    62  func (as *Assumptions) AddUnrestrictedPaths(paths ...string) {
    63  	as.unrestrictedPaths = append(as.unrestrictedPaths, paths...)
    64  }
    65  
    66  // AddModeHint adds a path glob and mode used when creating path elements.
    67  func (as *Assumptions) AddModeHint(pathGlob string, mode os.FileMode) {
    68  	as.modeHints = append(as.modeHints, ModeHint{PathGlob: pathGlob, Mode: mode})
    69  }
    70  
    71  // ModeForPath returns the mode for creating a directory at a given path.
    72  //
    73  // The default mode is 0755 but AddModeHint calls can influence the mode at a
    74  // specific path. When matching path elements, "*" does not match the directory
    75  // separator. In effect it can only be used as a wildcard for a specific
    76  // directory name. This constraint makes hints easier to model in practice.
    77  //
    78  // When multiple hints match the given path, ModeForPath panics.
    79  func (as *Assumptions) ModeForPath(path string) os.FileMode {
    80  	mode := os.FileMode(0755)
    81  	var foundHint *ModeHint
    82  	for _, hint := range as.modeHints {
    83  		if ok, _ := filepath.Match(hint.PathGlob, path); ok {
    84  			if foundHint == nil {
    85  				mode = hint.Mode
    86  				foundHint = &hint
    87  			} else {
    88  				panic(fmt.Errorf("cannot find unique mode for path %q: %q and %q both provide hints",
    89  					path, foundHint.PathGlob, foundHint.PathGlob))
    90  			}
    91  		}
    92  	}
    93  	return mode
    94  }
    95  
    96  // isRestricted checks whether a path falls under restricted writing scheme.
    97  //
    98  // Provided path is the full, absolute path of the entity that needs to be
    99  // created (directory, file or symbolic link).
   100  func (as *Assumptions) isRestricted(path string) bool {
   101  	// Anything rooted at one of the unrestricted paths is not restricted.
   102  	// Those are for things like /var/snap/, for example.
   103  	for _, p := range as.unrestrictedPaths {
   104  		if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") {
   105  			return false
   106  		}
   107  
   108  	}
   109  	// All other paths are restricted
   110  	return true
   111  }
   112  
   113  // MockUnrestrictedPaths replaces the set of path paths without any restrictions.
   114  func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) {
   115  	old := as.unrestrictedPaths
   116  	as.unrestrictedPaths = paths
   117  	return func() {
   118  		as.unrestrictedPaths = old
   119  	}
   120  }
   121  
   122  // AddChange records the fact that a change was applied to the system.
   123  func (as *Assumptions) AddChange(change *Change) {
   124  	as.pastChanges = append(as.pastChanges, change)
   125  }
   126  
   127  // canWriteToDirectory returns true if writing to a given directory is allowed.
   128  //
   129  // Writing is allowed in one of thee cases:
   130  // 1) The directory is in one of the explicitly permitted locations.
   131  //    This is the strongest permission as it explicitly allows writing to
   132  //    places that may show up on the host, one of the examples being $SNAP_DATA.
   133  // 2) The directory is on a read-only filesystem.
   134  // 3) The directory is on a tmpfs created by snapd.
   135  func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) {
   136  	if !as.isRestricted(dirName) {
   137  		return true, nil
   138  	}
   139  	var fsData syscall.Statfs_t
   140  	if err := sysFstatfs(dirFd, &fsData); err != nil {
   141  		return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err)
   142  	}
   143  	var fileData syscall.Stat_t
   144  	if err := sysFstat(dirFd, &fileData); err != nil {
   145  		return false, fmt.Errorf("cannot fstat %q: %s", dirName, err)
   146  	}
   147  	// Writing to read only directories is allowed because EROFS is handled
   148  	// by each of the writing helpers already.
   149  	if ok := isReadOnly(dirName, &fsData); ok {
   150  		return true, nil
   151  	}
   152  	// Writing to a trusted tmpfs is allowed because those are not leaking to
   153  	// the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor,
   154  	if as.verifiedDevices[fileData.Dev] {
   155  		return true, nil
   156  	}
   157  	if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok {
   158  		if as.verifiedDevices == nil {
   159  			as.verifiedDevices = make(map[uint64]bool)
   160  		}
   161  		// Don't record 0:0 as those are all to easy to add in tests and would
   162  		// skew tests using zero-initialized structures. Real device numbers
   163  		// are not zero either so this is not a test-only conditional.
   164  		if fileData.Dev != 0 {
   165  			as.verifiedDevices[fileData.Dev] = true
   166  		}
   167  		return true, nil
   168  	}
   169  	// If writing is not not allowed by one of the three rules above then it is
   170  	// disallowed.
   171  	return false, nil
   172  }
   173  
   174  // RestrictionsFor computes restrictions for the desired path.
   175  func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions {
   176  	// Writing to a restricted path results in step-by-step validation of each
   177  	// directory, starting from the root of the file system. Unless writing is
   178  	// allowed a mimic must be constructed to ensure that writes are not visible in
   179  	// undesired locations of the host filesystem.
   180  	if as.isRestricted(desiredPath) {
   181  		return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true}
   182  	}
   183  	return nil
   184  }
   185  
   186  // Restrictions contains meta-data of a compound write operation.
   187  //
   188  // This structure helps functions that write to the filesystem to keep track of
   189  // the ultimate destination across several calls (e.g. the function that
   190  // creates a file needs to call helpers to create subsequent directories).
   191  // Keeping track of the desired path aids in constructing useful error
   192  // messages.
   193  //
   194  // In addition the structure keeps track of the restricted write mode flag which
   195  // is based on the full path of the desired object being constructed. This allows
   196  // various write helpers to avoid trespassing on host filesystem in places that
   197  // are not expected to be written to by snapd (e.g. outside of $SNAP_DATA).
   198  type Restrictions struct {
   199  	assumptions *Assumptions
   200  	desiredPath string
   201  	restricted  bool
   202  }
   203  
   204  // Check verifies whether writing to a directory would trespass on the host.
   205  //
   206  // The check is only performed in restricted mode. If the check fails a
   207  // TrespassingError is returned.
   208  func (rs *Restrictions) Check(dirFd int, dirName string) error {
   209  	if rs == nil || !rs.restricted {
   210  		return nil
   211  	}
   212  	// In restricted mode check the directory before attempting to write to it.
   213  	ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName)
   214  	if ok || err != nil {
   215  		return err
   216  	}
   217  	if dirName == "/" {
   218  		// If writing to / is not allowed then we are in a tough spot because
   219  		// we cannot construct a writable mimic over /. This should never
   220  		// happen in normal circumstances because the root filesystem is some
   221  		// kind of base snap.
   222  		return fmt.Errorf("cannot recover from trespassing over /")
   223  	}
   224  	logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath)
   225  	logger.Debugf("restricted mode: %#v", rs.restricted)
   226  	logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths)
   227  	logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices)
   228  	logger.Debugf("past changes: %v", rs.assumptions.pastChanges)
   229  	return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath}
   230  }
   231  
   232  // Lift lifts write restrictions for the desired path.
   233  //
   234  // This function should be called when, as subsequent components of a path are
   235  // either discovered or created, the conditions for using restricted mode are
   236  // no longer true.
   237  func (rs *Restrictions) Lift() {
   238  	if rs != nil {
   239  		rs.restricted = false
   240  	}
   241  }
   242  
   243  // TrespassingError is an error when filesystem operation would affect the host.
   244  type TrespassingError struct {
   245  	ViolatedPath string
   246  	DesiredPath  string
   247  }
   248  
   249  // Error returns a formatted error message.
   250  func (e *TrespassingError) Error() string {
   251  	return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath)
   252  }
   253  
   254  // isReadOnly checks whether the underlying filesystem is read only or is mounted as such.
   255  func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool {
   256  	// If something is mounted with f_flags & ST_RDONLY then is read-only.
   257  	if fsData.Flags&StReadOnly == StReadOnly {
   258  		return true
   259  	}
   260  	// If something is a known read-only file-system then it is safe.
   261  	// Older copies of snapd were not mounting squashfs as read only.
   262  	if fsData.Type == SquashfsMagic {
   263  		return true
   264  	}
   265  	return false
   266  }
   267  
   268  // isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd
   269  //
   270  // The function inspects the directory and a list of changes that were applied
   271  // to the mount namespace. A directory is trusted if it is a tmpfs that was
   272  // mounted by snap-confine or snapd-update-ns. Note that sub-directories of a
   273  // trusted tmpfs are not considered trusted by this function.
   274  func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool {
   275  	// If something is not a tmpfs it cannot be the trusted tmpfs we are looking for.
   276  	if fsData.Type != TmpfsMagic {
   277  		return false
   278  	}
   279  	// Any of the past changes that mounted a tmpfs exactly at the directory we
   280  	// are inspecting is considered as trusted. This is conservative because it
   281  	// doesn't trust sub-directories of a trusted tmpfs. This approach is
   282  	// sufficient for the intended use.
   283  	//
   284  	// The algorithm goes over all the changes in reverse and picks up the
   285  	// first tmpfs mount or unmount action that matches the directory name.
   286  	// The set of constraints in snap-update-ns and snapd prevent from mounting
   287  	// over an existing mount point so we don't need to consider e.g. a bind
   288  	// mount shadowing an active tmpfs.
   289  	for i := len(changes) - 1; i >= 0; i-- {
   290  		change := changes[i]
   291  		if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName {
   292  			return change.Action == Mount || change.Action == Keep
   293  		}
   294  	}
   295  	return false
   296  }