github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/snap/squashfs/squashfs.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-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 squashfs
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path"
    31  	"path/filepath"
    32  	"regexp"
    33  	"strings"
    34  	"syscall"
    35  	"time"
    36  
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/logger"
    39  	"github.com/snapcore/snapd/osutil"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/snap/internal"
    42  	"github.com/snapcore/snapd/snapdtool"
    43  	"github.com/snapcore/snapd/strutil"
    44  )
    45  
    46  const (
    47  	// https://github.com/plougher/squashfs-tools/blob/master/squashfs-tools/squashfs_fs.h#L289
    48  	superblockSize = 96
    49  )
    50  
    51  var (
    52  	// magic is the magic prefix of squashfs snap files.
    53  	magic = []byte{'h', 's', 'q', 's'}
    54  
    55  	// for testing
    56  	isRootWritableOverlay = osutil.IsRootWritableOverlay
    57  )
    58  
    59  func FileHasSquashfsHeader(path string) bool {
    60  	f, err := os.Open(path)
    61  	if err != nil {
    62  		return false
    63  	}
    64  	defer f.Close()
    65  
    66  	// a squashfs file would contain at least the superblock + some data
    67  	header := make([]byte, superblockSize+1)
    68  	if _, err := f.ReadAt(header, 0); err != nil {
    69  		return false
    70  	}
    71  
    72  	return bytes.HasPrefix(header, magic)
    73  }
    74  
    75  // Snap is the squashfs based snap.
    76  type Snap struct {
    77  	path string
    78  }
    79  
    80  // Path returns the path of the backing file.
    81  func (s *Snap) Path() string {
    82  	return s.path
    83  }
    84  
    85  // New returns a new Squashfs snap.
    86  func New(snapPath string) *Snap {
    87  	return &Snap{path: snapPath}
    88  }
    89  
    90  var osLink = os.Link
    91  var snapdtoolCommandFromSystemSnap = snapdtool.CommandFromSystemSnap
    92  
    93  // Install installs a squashfs snap file through an appropriate method.
    94  func (s *Snap) Install(targetPath, mountDir string, opts *snap.InstallOptions) (bool, error) {
    95  
    96  	// ensure mount-point and blob target dir.
    97  	for _, dir := range []string{mountDir, filepath.Dir(targetPath)} {
    98  		if err := os.MkdirAll(dir, 0755); err != nil {
    99  			return false, err
   100  		}
   101  	}
   102  
   103  	// This is required so that the tests can simulate a mounted
   104  	// snap when we "install" a squashfs snap in the tests.
   105  	// We can not mount it for real in the tests, so we just unpack
   106  	// it to the location which is good enough for the tests.
   107  	if osutil.GetenvBool("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") {
   108  		if err := s.Unpack("*", mountDir); err != nil {
   109  			return false, err
   110  		}
   111  	}
   112  
   113  	// nothing to do, happens on e.g. first-boot when we already
   114  	// booted with the OS snap but its also in the seed.yaml
   115  	if s.path == targetPath || osutil.FilesAreEqual(s.path, targetPath) {
   116  		didNothing := true
   117  		return didNothing, nil
   118  	}
   119  
   120  	overlayRoot, err := isRootWritableOverlay()
   121  	if err != nil {
   122  		logger.Noticef("cannot detect root filesystem on overlay: %v", err)
   123  	}
   124  	// Hard-linking on overlayfs is identical to a full blown
   125  	// copy.  When we are operating on a overlayfs based system (e.g. live
   126  	// installer) use symbolic links.
   127  	// https://bugs.launchpad.net/snapd/+bug/1867415
   128  	if overlayRoot == "" {
   129  		// try to (hard)link the file, but go on to trying to copy it
   130  		// if it fails for whatever reason
   131  		//
   132  		// link(2) returns EPERM on filesystems that don't support
   133  		// hard links (like vfat), so checking the error here doesn't
   134  		// make sense vs just trying to copy it.
   135  		if err := osLink(s.path, targetPath); err == nil {
   136  			return false, nil
   137  		}
   138  	}
   139  
   140  	// if the installation must not cross devices, then we should not use
   141  	// symlinks and instead must copy the file entirely, this is the case
   142  	// during seeding on uc20 in run mode for example
   143  	if opts == nil || !opts.MustNotCrossDevices {
   144  		// if the source snap file is in seed, but the hardlink failed, symlinking
   145  		// it saves the copy (which in livecd is expensive) so try that next
   146  		// note that on UC20, the snap file could be in a deep subdir of
   147  		// SnapSeedDir, i.e. /var/lib/snapd/seed/systems/20200521/snaps/<name>.snap
   148  		// so we need to check if it has the prefix of the seed dir
   149  		cleanSrc := filepath.Clean(s.path)
   150  		if strings.HasPrefix(cleanSrc, dirs.SnapSeedDir) {
   151  			if os.Symlink(s.path, targetPath) == nil {
   152  				return false, nil
   153  			}
   154  		}
   155  	}
   156  
   157  	return false, osutil.CopyFile(s.path, targetPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
   158  }
   159  
   160  // unsquashfsStderrWriter is a helper that captures errors from
   161  // unsquashfs on stderr. Because unsquashfs will potentially
   162  // (e.g. on out-of-diskspace) report an error on every single
   163  // file we limit the reported error lines to 4.
   164  //
   165  // unsquashfs does not exit with an exit code for write errors
   166  // (e.g. no space left on device). There is an upstream PR
   167  // to fix this https://github.com/plougher/squashfs-tools/pull/46
   168  //
   169  // However in the meantime we can detect errors by looking
   170  // on stderr for "failed" which is pretty consistently used in
   171  // the unsquashfs.c source in case of errors.
   172  type unsquashfsStderrWriter struct {
   173  	strutil.MatchCounter
   174  }
   175  
   176  var unsquashfsStderrRegexp = regexp.MustCompile(`(?m).*\b[Ff]ailed\b.*`)
   177  
   178  func newUnsquashfsStderrWriter() *unsquashfsStderrWriter {
   179  	return &unsquashfsStderrWriter{strutil.MatchCounter{
   180  		Regexp: unsquashfsStderrRegexp,
   181  		N:      4, // note Err below uses this value
   182  	}}
   183  }
   184  
   185  func (u *unsquashfsStderrWriter) Err() error {
   186  	// here we use that our N is 4.
   187  	errors, count := u.Matches()
   188  	switch count {
   189  	case 0:
   190  		return nil
   191  	case 1:
   192  		return fmt.Errorf("failed: %q", errors[0])
   193  	case 2, 3, 4:
   194  		return fmt.Errorf("failed: %s, and %q", strutil.Quoted(errors[:len(errors)-1]), errors[len(errors)-1])
   195  	default:
   196  		// count > len(matches)
   197  		extra := count - len(errors)
   198  		return fmt.Errorf("failed: %s, and %d more", strutil.Quoted(errors), extra)
   199  	}
   200  }
   201  
   202  func (s *Snap) Unpack(src, dstDir string) error {
   203  	usw := newUnsquashfsStderrWriter()
   204  
   205  	cmd := exec.Command("unsquashfs", "-n", "-f", "-d", dstDir, s.path, src)
   206  	cmd.Stderr = usw
   207  	if err := cmd.Run(); err != nil {
   208  		return err
   209  	}
   210  	if usw.Err() != nil {
   211  		return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, usw.Err())
   212  	}
   213  	return nil
   214  }
   215  
   216  // Size returns the size of a squashfs snap.
   217  func (s *Snap) Size() (size int64, err error) {
   218  	st, err := os.Stat(s.path)
   219  	if err != nil {
   220  		return 0, err
   221  	}
   222  
   223  	return st.Size(), nil
   224  }
   225  
   226  func (s *Snap) withUnpackedFile(filePath string, f func(p string) error) error {
   227  	tmpdir, err := ioutil.TempDir("", "read-file")
   228  	if err != nil {
   229  		return err
   230  	}
   231  	defer os.RemoveAll(tmpdir)
   232  
   233  	unpackDir := filepath.Join(tmpdir, "unpack")
   234  	if output, err := exec.Command("unsquashfs", "-n", "-i", "-d", unpackDir, s.path, filePath).CombinedOutput(); err != nil {
   235  		return fmt.Errorf("cannot run unsquashfs: %v", osutil.OutputErr(output, err))
   236  	}
   237  
   238  	return f(filepath.Join(unpackDir, filePath))
   239  }
   240  
   241  // RandomAccessFile returns an implementation to read at any given
   242  // location for a single file inside the squashfs snap plus
   243  // information about the file size.
   244  func (s *Snap) RandomAccessFile(filePath string) (interface {
   245  	io.ReaderAt
   246  	io.Closer
   247  	Size() int64
   248  }, error) {
   249  	var f *os.File
   250  	err := s.withUnpackedFile(filePath, func(p string) (err error) {
   251  		f, err = os.Open(p)
   252  		return
   253  	})
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	return internal.NewSizedFile(f)
   258  }
   259  
   260  // ReadFile returns the content of a single file inside a squashfs snap.
   261  func (s *Snap) ReadFile(filePath string) (content []byte, err error) {
   262  	err = s.withUnpackedFile(filePath, func(p string) (err error) {
   263  		content, err = ioutil.ReadFile(p)
   264  		return
   265  	})
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return content, nil
   270  }
   271  
   272  // skipper is used to track directories that should be skipped
   273  //
   274  // Given sk := make(skipper), if you sk.Add("foo/bar"), then
   275  // sk.Has("foo/bar") is true, but also sk.Has("foo/bar/baz")
   276  //
   277  // It could also be a map[string]bool, but because it's only supposed
   278  // to be checked through its Has method as above, the small added
   279  // complexity of it being a map[string]struct{} lose to the associated
   280  // space savings.
   281  type skipper map[string]struct{}
   282  
   283  func (sk skipper) Add(path string) {
   284  	sk[filepath.Clean(path)] = struct{}{}
   285  }
   286  
   287  func (sk skipper) Has(path string) bool {
   288  	for p := filepath.Clean(path); p != "." && p != "/"; p = filepath.Dir(p) {
   289  		if _, ok := sk[p]; ok {
   290  			return true
   291  		}
   292  	}
   293  
   294  	return false
   295  }
   296  
   297  // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee.
   298  func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error {
   299  	relative = filepath.Clean(relative)
   300  	if relative == "" || relative == "/" {
   301  		relative = "."
   302  	} else if relative[0] == '/' {
   303  		// I said relative, darn it :-)
   304  		relative = relative[1:]
   305  	}
   306  
   307  	var cmd *exec.Cmd
   308  	if relative == "." {
   309  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path)
   310  	} else {
   311  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative)
   312  	}
   313  	cmd.Env = []string{"TZ=UTC"}
   314  	stdout, err := cmd.StdoutPipe()
   315  	if err != nil {
   316  		return walkFn(relative, nil, err)
   317  	}
   318  	if err := cmd.Start(); err != nil {
   319  		return walkFn(relative, nil, err)
   320  	}
   321  	defer cmd.Process.Kill()
   322  
   323  	scanner := bufio.NewScanner(stdout)
   324  	// skip the header
   325  	for scanner.Scan() {
   326  		if len(scanner.Bytes()) == 0 {
   327  			break
   328  		}
   329  	}
   330  
   331  	skipper := make(skipper)
   332  	for scanner.Scan() {
   333  		st, err := fromRaw(scanner.Bytes())
   334  		if err != nil {
   335  			err = walkFn(relative, nil, err)
   336  			if err != nil {
   337  				return err
   338  			}
   339  		} else {
   340  			path := filepath.Join(relative, st.Path())
   341  			if skipper.Has(path) {
   342  				continue
   343  			}
   344  			err = walkFn(path, st, nil)
   345  			if err != nil {
   346  				if err == filepath.SkipDir && st.IsDir() {
   347  					skipper.Add(path)
   348  				} else {
   349  					return err
   350  				}
   351  			}
   352  		}
   353  	}
   354  
   355  	if err := scanner.Err(); err != nil {
   356  		return walkFn(relative, nil, err)
   357  	}
   358  
   359  	if err := cmd.Wait(); err != nil {
   360  		return walkFn(relative, nil, err)
   361  	}
   362  	return nil
   363  }
   364  
   365  // ListDir returns the content of a single directory inside a squashfs snap.
   366  func (s *Snap) ListDir(dirPath string) ([]string, error) {
   367  	output, err := exec.Command(
   368  		"unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput()
   369  	if err != nil {
   370  		return nil, osutil.OutputErr(output, err)
   371  	}
   372  
   373  	prefixPath := path.Join("_", dirPath)
   374  	pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$")
   375  	if err != nil {
   376  		return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err)
   377  	}
   378  
   379  	var directoryContents []string
   380  	for _, groups := range pattern.FindAllSubmatch(output, -1) {
   381  		if len(groups) > 1 {
   382  			directoryContents = append(directoryContents, string(groups[1]))
   383  		}
   384  	}
   385  
   386  	return directoryContents, nil
   387  }
   388  
   389  const maxErrPaths = 10
   390  
   391  type errPathsNotReadable struct {
   392  	paths []string
   393  }
   394  
   395  func (e *errPathsNotReadable) accumulate(p string, fi os.FileInfo) error {
   396  	if len(e.paths) >= maxErrPaths {
   397  		return e
   398  	}
   399  	if st, ok := fi.Sys().(*syscall.Stat_t); ok {
   400  		e.paths = append(e.paths, fmt.Sprintf("%s (owner %v:%v mode %#03o)", p, st.Uid, st.Gid, fi.Mode().Perm()))
   401  	} else {
   402  		e.paths = append(e.paths, p)
   403  	}
   404  	return nil
   405  }
   406  
   407  func (e *errPathsNotReadable) asErr() error {
   408  	if len(e.paths) > 0 {
   409  		return e
   410  	}
   411  	return nil
   412  }
   413  
   414  func (e *errPathsNotReadable) Error() string {
   415  	var b bytes.Buffer
   416  
   417  	b.WriteString("cannot access the following locations in the snap source directory:\n")
   418  	for _, p := range e.paths {
   419  		fmt.Fprintf(&b, "- ")
   420  		fmt.Fprintf(&b, p)
   421  		fmt.Fprintf(&b, "\n")
   422  	}
   423  	if len(e.paths) == maxErrPaths {
   424  		fmt.Fprintf(&b, "- too many errors, listing first %v entries\n", maxErrPaths)
   425  	}
   426  	return b.String()
   427  }
   428  
   429  // verifyContentAccessibleForBuild checks whether the content under source
   430  // directory is usable to the user and can be represented by mksquashfs.
   431  func verifyContentAccessibleForBuild(sourceDir string) error {
   432  	var errPaths errPathsNotReadable
   433  
   434  	withSlash := filepath.Clean(sourceDir) + "/"
   435  	err := filepath.Walk(withSlash, func(path string, st os.FileInfo, err error) error {
   436  		if err != nil {
   437  			if !os.IsPermission(err) {
   438  				return err
   439  			}
   440  			// accumulate permission errors
   441  			return errPaths.accumulate(strings.TrimPrefix(path, withSlash), st)
   442  		}
   443  		mode := st.Mode()
   444  		if !mode.IsRegular() && !mode.IsDir() {
   445  			// device nodes are just recreated by mksquashfs
   446  			return nil
   447  		}
   448  		if mode.IsRegular() && st.Size() == 0 {
   449  			// empty files are also recreated
   450  			return nil
   451  		}
   452  
   453  		f, err := os.Open(path)
   454  		if err != nil {
   455  			if !os.IsPermission(err) {
   456  				return err
   457  			}
   458  			// accumulate permission errors
   459  			if err = errPaths.accumulate(strings.TrimPrefix(path, withSlash), st); err != nil {
   460  				return err
   461  			}
   462  			// workaround for https://github.com/golang/go/issues/21758
   463  			// with pre 1.10 go, explicitly skip directory
   464  			if mode.IsDir() {
   465  				return filepath.SkipDir
   466  			}
   467  			return nil
   468  		}
   469  		f.Close()
   470  		return nil
   471  	})
   472  	if err != nil {
   473  		return err
   474  	}
   475  	return errPaths.asErr()
   476  }
   477  
   478  type MksquashfsError struct {
   479  	msg string
   480  }
   481  
   482  func (m MksquashfsError) Error() string {
   483  	return m.msg
   484  }
   485  
   486  type BuildOpts struct {
   487  	SnapType     string
   488  	Compression  string
   489  	ExcludeFiles []string
   490  }
   491  
   492  // Build builds the snap.
   493  func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
   494  	if opts == nil {
   495  		opts = &BuildOpts{}
   496  	}
   497  	if err := verifyContentAccessibleForBuild(sourceDir); err != nil {
   498  		return err
   499  	}
   500  
   501  	fullSnapPath, err := filepath.Abs(s.path)
   502  	if err != nil {
   503  		return err
   504  	}
   505  	// default to xz
   506  	compression := opts.Compression
   507  	if compression == "" {
   508  		// TODO: support other compression options, xz is very
   509  		// slow for certain apps, see
   510  		// https://forum.snapcraft.io/t/squashfs-performance-effect-on-snap-startup-time/13920
   511  		compression = "xz"
   512  	}
   513  	cmd, err := snapdtoolCommandFromSystemSnap("/usr/bin/mksquashfs")
   514  	if err != nil {
   515  		cmd = exec.Command("mksquashfs")
   516  	}
   517  	cmd.Args = append(cmd.Args,
   518  		".", fullSnapPath,
   519  		"-noappend",
   520  		"-comp", compression,
   521  		"-no-fragments",
   522  		"-no-progress",
   523  	)
   524  	if len(opts.ExcludeFiles) > 0 {
   525  		cmd.Args = append(cmd.Args, "-wildcards")
   526  		for _, excludeFile := range opts.ExcludeFiles {
   527  			cmd.Args = append(cmd.Args, "-ef", excludeFile)
   528  		}
   529  	}
   530  	snapType := opts.SnapType
   531  	if snapType != "os" && snapType != "core" && snapType != "base" {
   532  		cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs")
   533  	}
   534  
   535  	return osutil.ChDir(sourceDir, func() error {
   536  		output, err := cmd.CombinedOutput()
   537  		if err != nil {
   538  			return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))}
   539  		}
   540  
   541  		return nil
   542  	})
   543  }
   544  
   545  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   546  func (s *Snap) BuildDate() time.Time {
   547  	return BuildDate(s.path)
   548  }
   549  
   550  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   551  func BuildDate(path string) time.Time {
   552  	var t0 time.Time
   553  
   554  	const prefix = "Creation or last append time "
   555  	m := &strutil.MatchCounter{
   556  		Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"),
   557  		N:      1,
   558  	}
   559  
   560  	cmd := exec.Command("unsquashfs", "-n", "-s", path)
   561  	cmd.Env = []string{"TZ=UTC"}
   562  	cmd.Stdout = m
   563  	cmd.Stderr = m
   564  	if err := cmd.Run(); err != nil {
   565  		return t0
   566  	}
   567  	matches, count := m.Matches()
   568  	if count != 1 {
   569  		return t0
   570  	}
   571  	t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):])
   572  	return t0
   573  }