github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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  	cmd := cmdCreateUser{
   283  		clientMixin: x.clientMixin,
   284  		Known:       true,
   285  		Sudoer:      true,
   286  	}
   287  	return cmd.Execute(nil)
   288  }
   289  
   290  func removableBlockDevices() (removableDevices []string) {
   291  	// eg. /sys/block/sda/removable
   292  	removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable"))
   293  	if err != nil {
   294  		return nil
   295  	}
   296  	for _, removableAttr := range removable {
   297  		val, err := ioutil.ReadFile(removableAttr)
   298  		if err != nil || string(val) != "1\n" {
   299  			// non removable
   300  			continue
   301  		}
   302  		// let's see if it has partitions
   303  		dev := filepath.Base(filepath.Dir(removableAttr))
   304  
   305  		pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev)
   306  		// eg. /sys/block/sda/sda1/partition
   307  		partitionAttrs, _ := filepath.Glob(pattern)
   308  
   309  		if len(partitionAttrs) == 0 {
   310  			// not partitioned? try to use the main device
   311  			removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev))
   312  			continue
   313  		}
   314  
   315  		for _, partAttr := range partitionAttrs {
   316  			val, err := ioutil.ReadFile(partAttr)
   317  			if err != nil || string(val) != "1\n" {
   318  				// non partition?
   319  				continue
   320  			}
   321  			pdev := filepath.Base(filepath.Dir(partAttr))
   322  			removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev))
   323  			// hasPartitions = true
   324  		}
   325  	}
   326  	sort.Strings(removableDevices)
   327  	return removableDevices
   328  }
   329  
   330  // inInstallmode returns true if it's UC20 system in install mode
   331  func inInstallMode() bool {
   332  	mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine()
   333  	if err != nil {
   334  		return false
   335  	}
   336  	return mode == "install"
   337  }
   338  
   339  func (x *cmdAutoImport) Execute(args []string) error {
   340  	if len(args) > 0 {
   341  		return ErrExtraArgs
   342  	}
   343  
   344  	if release.OnClassic && !x.ForceClassic {
   345  		fmt.Fprintf(Stderr, "auto-import is disabled on classic\n")
   346  		return nil
   347  	}
   348  	// TODO:UC20: workaround for LP: #1860231
   349  	if inInstallMode() {
   350  		fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n")
   351  		return nil
   352  	}
   353  
   354  	devices := x.Mount
   355  	if len(devices) == 0 {
   356  		// coldplug scenario, try all removable devices
   357  		devices = removableBlockDevices()
   358  	}
   359  
   360  	for _, path := range devices {
   361  		// udev adds new /dev/loopX devices on the fly when a
   362  		// loop mount happens and there is no loop device left.
   363  		//
   364  		// We need to ignore these events because otherwise both
   365  		// our mount and the "mount -o loop" fight over the same
   366  		// device and we get nasty errors
   367  		if strings.HasPrefix(path, "/dev/loop") {
   368  			continue
   369  		}
   370  
   371  		mp, err := tryMount(path)
   372  		if err != nil {
   373  			continue // Error was reported. Continue looking.
   374  		}
   375  		defer doUmount(mp)
   376  	}
   377  
   378  	added1, err := autoImportFromSpool(x.client)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	added2, err := autoImportFromAllMounts(x.client)
   384  	if err != nil {
   385  		return err
   386  	}
   387  
   388  	if added1+added2 > 0 {
   389  		return x.autoAddUsers()
   390  	}
   391  
   392  	return nil
   393  }