github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/snap/container.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2015 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 snap
    21  
    22  import (
    23  	"errors"
    24  	"io"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  )
    29  
    30  // Container is the interface to interact with the low-level snap files.
    31  type Container interface {
    32  	// Size returns the size of the snap in bytes.
    33  	Size() (int64, error)
    34  
    35  	// RandomAccessFile returns an implementation to read at any
    36  	// given location for a single file inside the snap plus
    37  	// information about the file size.
    38  	RandomAccessFile(relative string) (interface {
    39  		io.ReaderAt
    40  		io.Closer
    41  		Size() int64
    42  	}, error)
    43  
    44  	// ReadFile returns the content of a single file from the snap.
    45  	ReadFile(relative string) ([]byte, error)
    46  
    47  	// Walk is like filepath.Walk, without the ordering guarantee.
    48  	Walk(relative string, walkFn filepath.WalkFunc) error
    49  
    50  	// ListDir returns the content of a single directory inside the snap.
    51  	ListDir(path string) ([]string, error)
    52  
    53  	// Install copies the snap file to targetPath (and possibly unpacks it to mountDir).
    54  	// The bool return value indicates if the backend had nothing to do on install.
    55  	Install(targetPath, mountDir string, opts *InstallOptions) (bool, error)
    56  
    57  	// Unpack unpacks the src parts to the dst directory
    58  	Unpack(src, dst string) error
    59  }
    60  
    61  // InstallOptions is for customizing the behavior of Install() from a higher
    62  // level function, i.e. from overlord customizing how a snap file is installed
    63  // on a system with tmpfs mounted as writable or with full disk encryption and
    64  // graded secured on UC20.
    65  type InstallOptions struct {
    66  	// MustNotCrossDevices indicates that the snap file when installed to the
    67  	// target must not cross devices. For example, installing a snap file from
    68  	// the ubuntu-seed partition onto the ubuntu-data partition must result in
    69  	// an installation on ubuntu-data that does not depend or reference
    70  	// ubuntu-seed at all.
    71  	MustNotCrossDevices bool
    72  }
    73  
    74  var (
    75  	// ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role
    76  	ErrBadModes = errors.New("snap is unusable due to bad permissions")
    77  	// ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories
    78  	ErrMissingPaths = errors.New("snap is unusable due to missing files")
    79  )
    80  
    81  // ValidateContainer does a minimal sanity check on the container.
    82  func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error {
    83  	// needsrx keeps track of things that need to have at least 0555 perms
    84  	needsrx := map[string]bool{
    85  		".":    true,
    86  		"meta": true,
    87  	}
    88  	// needsx keeps track of things that need to have at least 0111 perms
    89  	needsx := map[string]bool{}
    90  	// needsr keeps track of things that need to have at least 0444 perms
    91  	needsr := map[string]bool{
    92  		"meta/snap.yaml": true,
    93  	}
    94  	// needsf keeps track of things that need to be regular files (or symlinks to regular files)
    95  	needsf := map[string]bool{}
    96  	// noskipd tracks directories we want to descend into despite not being in needs*
    97  	noskipd := map[string]bool{}
    98  
    99  	for _, app := range s.Apps {
   100  		// for non-services, paths go into the needsrx bag because users
   101  		// need rx perms to execute it
   102  		bag := needsrx
   103  		paths := []string{app.Command}
   104  		if app.IsService() {
   105  			// services' paths just need to not be skipped by the validator
   106  			bag = noskipd
   107  			// additional paths to check for services:
   108  			// XXX maybe have a method on app to keep this in sync
   109  			paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand)
   110  		}
   111  
   112  		for _, path := range paths {
   113  			path = normPath(path)
   114  			if path == "" {
   115  				continue
   116  			}
   117  
   118  			needsf[path] = true
   119  			if app.IsService() {
   120  				needsx[path] = true
   121  			}
   122  			for ; path != "."; path = filepath.Dir(path) {
   123  				bag[path] = true
   124  			}
   125  		}
   126  
   127  		// completer is special :-/
   128  		if path := normPath(app.Completer); path != "" {
   129  			needsr[path] = true
   130  			for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) {
   131  				needsrx[path] = true
   132  			}
   133  		}
   134  	}
   135  	// note all needsr so far need to be regular files (or symlinks)
   136  	for k := range needsr {
   137  		needsf[k] = true
   138  	}
   139  	// thing can get jumbled up
   140  	for path := range needsrx {
   141  		delete(needsx, path)
   142  		delete(needsr, path)
   143  	}
   144  	for path := range needsx {
   145  		if needsr[path] {
   146  			delete(needsx, path)
   147  			delete(needsr, path)
   148  			needsrx[path] = true
   149  		}
   150  	}
   151  	seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr))
   152  
   153  	// bad modes are logged instead of being returned because the end user
   154  	// can do nothing with the info (and the developer can read the logs)
   155  	hasBadModes := false
   156  	err := c.Walk(".", func(path string, info os.FileInfo, err error) error {
   157  		if err != nil {
   158  			return err
   159  		}
   160  
   161  		mode := info.Mode()
   162  		if needsrx[path] || needsx[path] || needsr[path] {
   163  			seen[path] = true
   164  		}
   165  		if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") {
   166  			if mode.IsDir() {
   167  				if noskipd[path] {
   168  					return nil
   169  				}
   170  				return filepath.SkipDir
   171  			}
   172  			return nil
   173  		}
   174  
   175  		if needsrx[path] || mode.IsDir() {
   176  			if mode.Perm()&0555 != 0555 {
   177  				logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode)
   178  				hasBadModes = true
   179  			}
   180  		} else {
   181  			if needsf[path] {
   182  				// this assumes that if it's a symlink it's OK. Arguably we
   183  				// should instead follow the symlink.  We'd have to expose
   184  				// Lstat(), and guard against loops, and ...  huge can of
   185  				// worms, and as this validator is meant as a developer aid
   186  				// more than anything else, not worth it IMHO (as I can't
   187  				// imagine this happening by accident).
   188  				if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
   189  					logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path)
   190  					hasBadModes = true
   191  				}
   192  			}
   193  			if needsx[path] || strings.HasPrefix(path, "meta/hooks/") {
   194  				if mode.Perm()&0111 == 0 {
   195  					logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode)
   196  					hasBadModes = true
   197  				}
   198  			} else {
   199  				// in needsr, or under meta but not a hook
   200  				if mode.Perm()&0444 != 0444 {
   201  					logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode)
   202  					hasBadModes = true
   203  				}
   204  			}
   205  		}
   206  		return nil
   207  	})
   208  	if err != nil {
   209  		return err
   210  	}
   211  	if len(seen) != len(needsx)+len(needsrx)+len(needsr) {
   212  		for _, needs := range []map[string]bool{needsx, needsrx, needsr} {
   213  			for path := range needs {
   214  				if !seen[path] {
   215  					logf("in snap %q: path %q does not exist", s.InstanceName(), path)
   216  				}
   217  			}
   218  		}
   219  		return ErrMissingPaths
   220  	}
   221  
   222  	if hasBadModes {
   223  		return ErrBadModes
   224  	}
   225  	return nil
   226  }
   227  
   228  // normPath is a helper for validateContainer. It takes a relative path (e.g. an
   229  // app's RestartCommand, which might be empty to mean there is no such thing),
   230  // and cleans it.
   231  //
   232  // * empty paths are returned as is
   233  // * if the path is not relative, it's initial / is dropped
   234  // * if the path goes "outside" (ie starts with ../), the empty string is
   235  //   returned (i.e. "ignore")
   236  // * if there's a space in the command, ignore the rest of the string
   237  //   (see also cmd/snap-exec/main.go's comment about strings.Split)
   238  func normPath(path string) string {
   239  	if path == "" {
   240  		return ""
   241  	}
   242  
   243  	path = strings.TrimPrefix(filepath.Clean(path), "/")
   244  	if strings.HasPrefix(path, "../") {
   245  		// not something inside the snap
   246  		return ""
   247  	}
   248  	if idx := strings.IndexByte(path, ' '); idx > -1 {
   249  		return path[:idx]
   250  	}
   251  
   252  	return path
   253  }