github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/cmd/snap/cmd_auto_import.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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  	"bufio"
    24  	"crypto"
    25  	"encoding/base64"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"sort"
    32  	"strings"
    33  	"syscall"
    34  
    35  	"github.com/jessevdk/go-flags"
    36  
    37  	"github.com/snapcore/snapd/boot"
    38  	"github.com/snapcore/snapd/client"
    39  	"github.com/snapcore/snapd/dirs"
    40  	"github.com/snapcore/snapd/i18n"
    41  	"github.com/snapcore/snapd/logger"
    42  	"github.com/snapcore/snapd/osutil"
    43  	"github.com/snapcore/snapd/release"
    44  	"github.com/snapcore/snapd/snapdenv"
    45  )
    46  
    47  const autoImportsName = "auto-import.assert"
    48  
    49  var mountInfoPath = "/proc/self/mountinfo"
    50  
    51  func autoImportCandidates() ([]string, error) {
    52  	var cands []string
    53  
    54  	// see https://www.kernel.org/doc/Documentation/filesystems/proc.txt,
    55  	// sec. 3.5
    56  	f, err := os.Open(mountInfoPath)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	defer f.Close()
    61  
    62  	isTesting := snapdenv.Testing()
    63  
    64  	// TODO: re-write this to use osutil.LoadMountInfo instead of doing the
    65  	//       parsing ourselves
    66  
    67  	scanner := bufio.NewScanner(f)
    68  	for scanner.Scan() {
    69  		l := strings.Fields(scanner.Text())
    70  
    71  		// Per proc.txt:3.5, /proc/<pid>/mountinfo looks like
    72  		//
    73  		//  36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
    74  		//  (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
    75  		//
    76  		// and (7) has zero or more elements, find the "-" separator.
    77  		i := 6
    78  		for i < len(l) && l[i] != "-" {
    79  			i++
    80  		}
    81  		if i+2 >= len(l) {
    82  			continue
    83  		}
    84  
    85  		mountSrc := l[i+2]
    86  
    87  		// skip everything that is not a device (cgroups, debugfs etc)
    88  		if !strings.HasPrefix(mountSrc, "/dev/") {
    89  			continue
    90  		}
    91  		// skip all loop devices (snaps)
    92  		if strings.HasPrefix(mountSrc, "/dev/loop") {
    93  			continue
    94  		}
    95  		// skip all ram disks (unless in tests)
    96  		if !isTesting && strings.HasPrefix(mountSrc, "/dev/ram") {
    97  			continue
    98  		}
    99  
   100  		// TODO: should the following 2 checks try to be more smart like
   101  		//       `snap-bootstrap initramfs-mounts` and try to find the boot disk
   102  		//       and determine what partitions to skip using the disks package?
   103  
   104  		// skip all initramfs mounted disks on uc20
   105  		mountPoint := l[4]
   106  		if strings.HasPrefix(mountPoint, boot.InitramfsRunMntDir) {
   107  			continue
   108  		}
   109  
   110  		// skip all seed dir mount points too, as these are bind mounts to the
   111  		// initramfs dirs on uc20, this can show up as
   112  		// /writable/system-data/var/lib/snapd/seed as well as
   113  		// /var/lib/snapd/seed
   114  		if strings.HasSuffix(mountPoint, dirs.SnapSeedDir) {
   115  			continue
   116  		}
   117  
   118  		cand := filepath.Join(mountPoint, autoImportsName)
   119  		if osutil.FileExists(cand) {
   120  			cands = append(cands, cand)
   121  		}
   122  	}
   123  
   124  	return cands, scanner.Err()
   125  }
   126  
   127  func queueFile(src string) error {
   128  	// refuse huge files, this is for assertions
   129  	fi, err := os.Stat(src)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	// 640kb ought be to enough for anyone
   134  	if fi.Size() > 640*1024 {
   135  		msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size())
   136  		logger.Noticef("error: %v", msg)
   137  		return msg
   138  	}
   139  
   140  	// ensure name is predictable, weak hash is ok
   141  	hash, _, err := osutil.FileDigest(src, crypto.SHA3_384)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash)))
   147  	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   148  		return err
   149  	}
   150  
   151  	return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite)
   152  }
   153  
   154  func autoImportFromSpool(cli *client.Client) (added int, err error) {
   155  	files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir)
   156  	if os.IsNotExist(err) {
   157  		return 0, nil
   158  	}
   159  	if err != nil {
   160  		return 0, err
   161  	}
   162  
   163  	for _, fi := range files {
   164  		cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name())
   165  		if err := ackFile(cli, cand); err != nil {
   166  			logger.Noticef("error: cannot import %s: %s", cand, err)
   167  			continue
   168  		} else {
   169  			logger.Noticef("imported %s", cand)
   170  			added++
   171  		}
   172  		// FIXME: only remove stuff older than N days?
   173  		if err := os.Remove(cand); err != nil {
   174  			return 0, err
   175  		}
   176  	}
   177  
   178  	return added, nil
   179  }
   180  
   181  func autoImportFromAllMounts(cli *client.Client) (int, error) {
   182  	cands, err := autoImportCandidates()
   183  	if err != nil {
   184  		return 0, err
   185  	}
   186  
   187  	added := 0
   188  	for _, cand := range cands {
   189  		err := ackFile(cli, cand)
   190  		// the server is not ready yet
   191  		if _, ok := err.(client.ConnectionError); ok {
   192  			logger.Noticef("queuing for later %s", cand)
   193  			if err := queueFile(cand); err != nil {
   194  				return 0, err
   195  			}
   196  			continue
   197  		}
   198  		if err != nil {
   199  			logger.Noticef("error: cannot import %s: %s", cand, err)
   200  			continue
   201  		} else {
   202  			logger.Noticef("imported %s", cand)
   203  		}
   204  		added++
   205  	}
   206  
   207  	return added, nil
   208  }
   209  
   210  var ioutilTempDir = ioutil.TempDir
   211  
   212  func tryMount(deviceName string) (string, error) {
   213  	tmpMountTarget, err := ioutilTempDir("", "snapd-auto-import-mount-")
   214  	if err != nil {
   215  		err = fmt.Errorf("cannot create temporary mount point: %v", err)
   216  		logger.Noticef("error: %v", err)
   217  		return "", err
   218  	}
   219  	// udev does not provide much environment ;)
   220  	if os.Getenv("PATH") == "" {
   221  		os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin")
   222  	}
   223  	// not using syscall.Mount() because we don't know the fs type in advance
   224  	cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget)
   225  	if output, err := cmd.CombinedOutput(); err != nil {
   226  		os.Remove(tmpMountTarget)
   227  		err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err))
   228  		logger.Noticef("error: %v", err)
   229  		return "", err
   230  	}
   231  
   232  	return tmpMountTarget, nil
   233  }
   234  
   235  var syscallUnmount = syscall.Unmount
   236  
   237  func doUmount(mp string) error {
   238  	if err := syscallUnmount(mp, 0); err != nil {
   239  		return err
   240  	}
   241  	return os.Remove(mp)
   242  }
   243  
   244  type cmdAutoImport struct {
   245  	clientMixin
   246  	Mount []string `long:"mount" arg-name:"<device path>"`
   247  
   248  	ForceClassic bool `long:"force-classic"`
   249  }
   250  
   251  var shortAutoImportHelp = i18n.G("Inspect devices for actionable information")
   252  
   253  var longAutoImportHelp = i18n.G(`
   254  The auto-import command searches available mounted devices looking for
   255  assertions that are signed by trusted authorities, and potentially
   256  performs system changes based on them.
   257  
   258  If one or more device paths are provided via --mount, these are temporarily
   259  mounted to be inspected as well. Even in that case the command will still
   260  consider all available mounted devices for inspection.
   261  
   262  Assertions to be imported must be made available in the auto-import.assert file
   263  in the root of the filesystem.
   264  `)
   265  
   266  func init() {
   267  	cmd := addCommand("auto-import",
   268  		shortAutoImportHelp,
   269  		longAutoImportHelp,
   270  		func() flags.Commander {
   271  			return &cmdAutoImport{}
   272  		}, map[string]string{
   273  			// TRANSLATORS: This should not start with a lowercase letter.
   274  			"mount": i18n.G("Temporarily mount device before inspecting"),
   275  			// TRANSLATORS: This should not start with a lowercase letter.
   276  			"force-classic": i18n.G("Force import on classic systems"),
   277  		}, nil)
   278  	cmd.hidden = true
   279  }
   280  
   281  func (x *cmdAutoImport) autoAddUsers() error {
   282  	options := client.CreateUserOptions{
   283  		Automatic: true,
   284  	}
   285  	results, err := x.client.CreateUsers([]*client.CreateUserOptions{&options})
   286  	for _, result := range results {
   287  		fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username)
   288  	}
   289  
   290  	return err
   291  }
   292  
   293  func removableBlockDevices() (removableDevices []string) {
   294  	// eg. /sys/block/sda/removable
   295  	removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable"))
   296  	if err != nil {
   297  		return nil
   298  	}
   299  	for _, removableAttr := range removable {
   300  		val, err := ioutil.ReadFile(removableAttr)
   301  		if err != nil || string(val) != "1\n" {
   302  			// non removable
   303  			continue
   304  		}
   305  		// let's see if it has partitions
   306  		dev := filepath.Base(filepath.Dir(removableAttr))
   307  
   308  		pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev)
   309  		// eg. /sys/block/sda/sda1/partition
   310  		partitionAttrs, _ := filepath.Glob(pattern)
   311  
   312  		if len(partitionAttrs) == 0 {
   313  			// not partitioned? try to use the main device
   314  			removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev))
   315  			continue
   316  		}
   317  
   318  		for _, partAttr := range partitionAttrs {
   319  			val, err := ioutil.ReadFile(partAttr)
   320  			if err != nil || string(val) != "1\n" {
   321  				// non partition?
   322  				continue
   323  			}
   324  			pdev := filepath.Base(filepath.Dir(partAttr))
   325  			removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev))
   326  			// hasPartitions = true
   327  		}
   328  	}
   329  	sort.Strings(removableDevices)
   330  	return removableDevices
   331  }
   332  
   333  // inInstallmode returns true if it's UC20 system in install mode
   334  func inInstallMode() bool {
   335  	mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine()
   336  	if err != nil {
   337  		return false
   338  	}
   339  	return mode == "install"
   340  }
   341  
   342  func (x *cmdAutoImport) Execute(args []string) error {
   343  	if len(args) > 0 {
   344  		return ErrExtraArgs
   345  	}
   346  
   347  	if release.OnClassic && !x.ForceClassic {
   348  		fmt.Fprintf(Stderr, "auto-import is disabled on classic\n")
   349  		return nil
   350  	}
   351  	// TODO:UC20: workaround for LP: #1860231
   352  	if inInstallMode() {
   353  		fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n")
   354  		return nil
   355  	}
   356  
   357  	devices := x.Mount
   358  	if len(devices) == 0 {
   359  		// coldplug scenario, try all removable devices
   360  		devices = removableBlockDevices()
   361  	}
   362  
   363  	for _, path := range devices {
   364  		// udev adds new /dev/loopX devices on the fly when a
   365  		// loop mount happens and there is no loop device left.
   366  		//
   367  		// We need to ignore these events because otherwise both
   368  		// our mount and the "mount -o loop" fight over the same
   369  		// device and we get nasty errors
   370  		if strings.HasPrefix(path, "/dev/loop") {
   371  			continue
   372  		}
   373  
   374  		mp, err := tryMount(path)
   375  		if err != nil {
   376  			continue // Error was reported. Continue looking.
   377  		}
   378  		defer doUmount(mp)
   379  	}
   380  
   381  	added1, err := autoImportFromSpool(x.client)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	added2, err := autoImportFromAllMounts(x.client)
   387  	if err != nil {
   388  		return err
   389  	}
   390  
   391  	if added1+added2 > 0 {
   392  		return x.autoAddUsers()
   393  	}
   394  
   395  	return nil
   396  }