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