github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  	"bytes"
    24  	"errors"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/snap/snapdir"
    32  	"github.com/snapcore/snapd/snap/squashfs"
    33  )
    34  
    35  // Container is the interface to interact with the low-level snap files.
    36  type Container interface {
    37  	// Size returns the size of the snap in bytes.
    38  	Size() (int64, error)
    39  
    40  	// ReadFile returns the content of a single file from the snap.
    41  	ReadFile(relative string) ([]byte, error)
    42  
    43  	// Walk is like filepath.Walk, without the ordering guarantee.
    44  	Walk(relative string, walkFn filepath.WalkFunc) error
    45  
    46  	// ListDir returns the content of a single directory inside the snap.
    47  	ListDir(path string) ([]string, error)
    48  
    49  	// Install copies the snap file to targetPath (and possibly unpacks it to mountDir)
    50  	Install(targetPath, mountDir string) error
    51  
    52  	// Unpack unpacks the src parts to the dst directory
    53  	Unpack(src, dst string) error
    54  }
    55  
    56  // backend implements a specific snap format
    57  type snapFormat struct {
    58  	magic []byte
    59  	open  func(fn string) (Container, error)
    60  }
    61  
    62  // formatHandlers is the registry of known formats, squashfs is the only one atm.
    63  var formatHandlers = []snapFormat{
    64  	{squashfs.Magic, func(p string) (Container, error) {
    65  		return squashfs.New(p), nil
    66  	}},
    67  }
    68  
    69  // Open opens a given snap file with the right backend.
    70  func Open(path string) (Container, error) {
    71  
    72  	if osutil.IsDirectory(path) {
    73  		if osutil.FileExists(filepath.Join(path, "meta", "snap.yaml")) {
    74  			return snapdir.New(path), nil
    75  		}
    76  
    77  		return nil, NotSnapError{Path: path}
    78  	}
    79  
    80  	// open the file and check magic
    81  	f, err := os.Open(path)
    82  	if err != nil {
    83  		return nil, fmt.Errorf("cannot open snap: %v", err)
    84  	}
    85  	defer f.Close()
    86  
    87  	header := make([]byte, 20)
    88  	if _, err := f.ReadAt(header, 0); err != nil {
    89  		return nil, fmt.Errorf("cannot read snap: %v", err)
    90  	}
    91  
    92  	for _, h := range formatHandlers {
    93  		if bytes.HasPrefix(header, h.magic) {
    94  			return h.open(path)
    95  		}
    96  	}
    97  
    98  	return nil, fmt.Errorf("cannot open snap: unknown header: %q", header)
    99  }
   100  
   101  var (
   102  	// ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role
   103  	ErrBadModes = errors.New("snap is unusable due to bad permissions")
   104  	// ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories
   105  	ErrMissingPaths = errors.New("snap is unusable due to missing files")
   106  )
   107  
   108  // ValidateContainer does a minimal sanity check on the container.
   109  func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error {
   110  	// needsrx keeps track of things that need to have at least 0555 perms
   111  	needsrx := map[string]bool{
   112  		".":    true,
   113  		"meta": true,
   114  	}
   115  	// needsx keeps track of things that need to have at least 0111 perms
   116  	needsx := map[string]bool{}
   117  	// needsr keeps track of things that need to have at least 0444 perms
   118  	needsr := map[string]bool{
   119  		"meta/snap.yaml": true,
   120  	}
   121  	// needsf keeps track of things that need to be regular files (or symlinks to regular files)
   122  	needsf := map[string]bool{}
   123  	// noskipd tracks directories we want to descend into despite not being in needs*
   124  	noskipd := map[string]bool{}
   125  
   126  	for _, app := range s.Apps {
   127  		// for non-services, paths go into the needsrx bag because users
   128  		// need rx perms to execute it
   129  		bag := needsrx
   130  		paths := []string{app.Command}
   131  		if app.IsService() {
   132  			// services' paths just need to not be skipped by the validator
   133  			bag = noskipd
   134  			// additional paths to check for services:
   135  			// XXX maybe have a method on app to keep this in sync
   136  			paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand)
   137  		}
   138  
   139  		for _, path := range paths {
   140  			path = normPath(path)
   141  			if path == "" {
   142  				continue
   143  			}
   144  
   145  			needsf[path] = true
   146  			if app.IsService() {
   147  				needsx[path] = true
   148  			}
   149  			for ; path != "."; path = filepath.Dir(path) {
   150  				bag[path] = true
   151  			}
   152  		}
   153  
   154  		// completer is special :-/
   155  		if path := normPath(app.Completer); path != "" {
   156  			needsr[path] = true
   157  			for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) {
   158  				needsrx[path] = true
   159  			}
   160  		}
   161  	}
   162  	// note all needsr so far need to be regular files (or symlinks)
   163  	for k := range needsr {
   164  		needsf[k] = true
   165  	}
   166  	// thing can get jumbled up
   167  	for path := range needsrx {
   168  		delete(needsx, path)
   169  		delete(needsr, path)
   170  	}
   171  	for path := range needsx {
   172  		if needsr[path] {
   173  			delete(needsx, path)
   174  			delete(needsr, path)
   175  			needsrx[path] = true
   176  		}
   177  	}
   178  	seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr))
   179  
   180  	// bad modes are logged instead of being returned because the end user
   181  	// can do nothing with the info (and the developer can read the logs)
   182  	hasBadModes := false
   183  	err := c.Walk(".", func(path string, info os.FileInfo, err error) error {
   184  		if err != nil {
   185  			return err
   186  		}
   187  
   188  		mode := info.Mode()
   189  		if needsrx[path] || needsx[path] || needsr[path] {
   190  			seen[path] = true
   191  		}
   192  		if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") {
   193  			if mode.IsDir() {
   194  				if noskipd[path] {
   195  					return nil
   196  				}
   197  				return filepath.SkipDir
   198  			}
   199  			return nil
   200  		}
   201  
   202  		if needsrx[path] || mode.IsDir() {
   203  			if mode.Perm()&0555 != 0555 {
   204  				logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode)
   205  				hasBadModes = true
   206  			}
   207  		} else {
   208  			if needsf[path] {
   209  				// this assumes that if it's a symlink it's OK. Arguably we
   210  				// should instead follow the symlink.  We'd have to expose
   211  				// Lstat(), and guard against loops, and ...  huge can of
   212  				// worms, and as this validator is meant as a developer aid
   213  				// more than anything else, not worth it IMHO (as I can't
   214  				// imagine this happening by accident).
   215  				if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
   216  					logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path)
   217  					hasBadModes = true
   218  				}
   219  			}
   220  			if needsx[path] || strings.HasPrefix(path, "meta/hooks/") {
   221  				if mode.Perm()&0111 == 0 {
   222  					logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode)
   223  					hasBadModes = true
   224  				}
   225  			} else {
   226  				// in needsr, or under meta but not a hook
   227  				if mode.Perm()&0444 != 0444 {
   228  					logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode)
   229  					hasBadModes = true
   230  				}
   231  			}
   232  		}
   233  		return nil
   234  	})
   235  	if err != nil {
   236  		return err
   237  	}
   238  	if len(seen) != len(needsx)+len(needsrx)+len(needsr) {
   239  		for _, needs := range []map[string]bool{needsx, needsrx, needsr} {
   240  			for path := range needs {
   241  				if !seen[path] {
   242  					logf("in snap %q: path %q does not exist", s.InstanceName(), path)
   243  				}
   244  			}
   245  		}
   246  		return ErrMissingPaths
   247  	}
   248  
   249  	if hasBadModes {
   250  		return ErrBadModes
   251  	}
   252  	return nil
   253  }
   254  
   255  // normPath is a helper for validateContainer. It takes a relative path (e.g. an
   256  // app's RestartCommand, which might be empty to mean there is no such thing),
   257  // and cleans it.
   258  //
   259  // * empty paths are returned as is
   260  // * if the path is not relative, it's initial / is dropped
   261  // * if the path goes "outside" (ie starts with ../), the empty string is
   262  //   returned (i.e. "ignore")
   263  // * if there's a space in the command, ignore the rest of the string
   264  //   (see also cmd/snap-exec/main.go's comment about strings.Split)
   265  func normPath(path string) string {
   266  	if path == "" {
   267  		return ""
   268  	}
   269  
   270  	path = strings.TrimPrefix(filepath.Clean(path), "/")
   271  	if strings.HasPrefix(path, "../") {
   272  		// not something inside the snap
   273  		return ""
   274  	}
   275  	if idx := strings.IndexByte(path, ' '); idx > -1 {
   276  		return path[:idx]
   277  	}
   278  
   279  	return path
   280  }