github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap/cmd_auto_import.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2016 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  	"strings"
    32  	"syscall"
    33  
    34  	"github.com/jessevdk/go-flags"
    35  
    36  	"github.com/snapcore/snapd/client"
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/i18n"
    39  	"github.com/snapcore/snapd/logger"
    40  	"github.com/snapcore/snapd/osutil"
    41  	"github.com/snapcore/snapd/release"
    42  )
    43  
    44  const autoImportsName = "auto-import.assert"
    45  
    46  var mountInfoPath = "/proc/self/mountinfo"
    47  
    48  func autoImportCandidates() ([]string, error) {
    49  	var cands []string
    50  
    51  	// see https://www.kernel.org/doc/Documentation/filesystems/proc.txt,
    52  	// sec. 3.5
    53  	f, err := os.Open(mountInfoPath)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	defer f.Close()
    58  
    59  	scanner := bufio.NewScanner(f)
    60  	for scanner.Scan() {
    61  		l := strings.Fields(scanner.Text())
    62  
    63  		// Per proc.txt:3.5, /proc/<pid>/mountinfo looks like
    64  		//
    65  		//  36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
    66  		//  (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
    67  		//
    68  		// and (7) has zero or more elements, find the "-" separator.
    69  		i := 6
    70  		for i < len(l) && l[i] != "-" {
    71  			i++
    72  		}
    73  		if i+2 >= len(l) {
    74  			continue
    75  		}
    76  
    77  		mountSrc := l[i+2]
    78  
    79  		// skip everything that is not a device (cgroups, debugfs etc)
    80  		if !strings.HasPrefix(mountSrc, "/dev/") {
    81  			continue
    82  		}
    83  		// skip all loop devices (snaps)
    84  		if strings.HasPrefix(mountSrc, "/dev/loop") {
    85  			continue
    86  		}
    87  		// skip all ram disks (unless in tests)
    88  		if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") {
    89  			continue
    90  		}
    91  
    92  		mountPoint := l[4]
    93  		cand := filepath.Join(mountPoint, autoImportsName)
    94  		if osutil.FileExists(cand) {
    95  			cands = append(cands, cand)
    96  		}
    97  	}
    98  
    99  	return cands, scanner.Err()
   100  
   101  }
   102  
   103  func queueFile(src string) error {
   104  	// refuse huge files, this is for assertions
   105  	fi, err := os.Stat(src)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	// 640kb ought be to enough for anyone
   110  	if fi.Size() > 640*1024 {
   111  		msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size())
   112  		logger.Noticef("error: %v", msg)
   113  		return msg
   114  	}
   115  
   116  	// ensure name is predictable, weak hash is ok
   117  	hash, _, err := osutil.FileDigest(src, crypto.SHA3_384)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash)))
   123  	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   124  		return err
   125  	}
   126  
   127  	return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite)
   128  }
   129  
   130  func autoImportFromSpool(cli *client.Client) (added int, err error) {
   131  	files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir)
   132  	if os.IsNotExist(err) {
   133  		return 0, nil
   134  	}
   135  	if err != nil {
   136  		return 0, err
   137  	}
   138  
   139  	for _, fi := range files {
   140  		cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name())
   141  		if err := ackFile(cli, cand); err != nil {
   142  			logger.Noticef("error: cannot import %s: %s", cand, err)
   143  			continue
   144  		} else {
   145  			logger.Noticef("imported %s", cand)
   146  			added++
   147  		}
   148  		// FIXME: only remove stuff older than N days?
   149  		if err := os.Remove(cand); err != nil {
   150  			return 0, err
   151  		}
   152  	}
   153  
   154  	return added, nil
   155  }
   156  
   157  func autoImportFromAllMounts(cli *client.Client) (int, error) {
   158  	cands, err := autoImportCandidates()
   159  	if err != nil {
   160  		return 0, err
   161  	}
   162  
   163  	added := 0
   164  	for _, cand := range cands {
   165  		err := ackFile(cli, cand)
   166  		// the server is not ready yet
   167  		if _, ok := err.(client.ConnectionError); ok {
   168  			logger.Noticef("queuing for later %s", cand)
   169  			if err := queueFile(cand); err != nil {
   170  				return 0, err
   171  			}
   172  			continue
   173  		}
   174  		if err != nil {
   175  			logger.Noticef("error: cannot import %s: %s", cand, err)
   176  			continue
   177  		} else {
   178  			logger.Noticef("imported %s", cand)
   179  		}
   180  		added++
   181  	}
   182  
   183  	return added, nil
   184  }
   185  
   186  func tryMount(deviceName string) (string, error) {
   187  	tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-")
   188  	if err != nil {
   189  		err = fmt.Errorf("cannot create temporary mount point: %v", err)
   190  		logger.Noticef("error: %v", err)
   191  		return "", err
   192  	}
   193  	// udev does not provide much environment ;)
   194  	if os.Getenv("PATH") == "" {
   195  		os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin")
   196  	}
   197  	// not using syscall.Mount() because we don't know the fs type in advance
   198  	cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget)
   199  	if output, err := cmd.CombinedOutput(); err != nil {
   200  		os.Remove(tmpMountTarget)
   201  		err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err))
   202  		logger.Noticef("error: %v", err)
   203  		return "", err
   204  	}
   205  
   206  	return tmpMountTarget, nil
   207  }
   208  
   209  func doUmount(mp string) error {
   210  	if err := syscall.Unmount(mp, 0); err != nil {
   211  		return err
   212  	}
   213  	return os.Remove(mp)
   214  }
   215  
   216  type cmdAutoImport struct {
   217  	clientMixin
   218  	Mount []string `long:"mount" arg-name:"<device path>"`
   219  
   220  	ForceClassic bool `long:"force-classic"`
   221  }
   222  
   223  var shortAutoImportHelp = i18n.G("Inspect devices for actionable information")
   224  
   225  var longAutoImportHelp = i18n.G(`
   226  The auto-import command searches available mounted devices looking for
   227  assertions that are signed by trusted authorities, and potentially
   228  performs system changes based on them.
   229  
   230  If one or more device paths are provided via --mount, these are temporarily
   231  mounted to be inspected as well. Even in that case the command will still
   232  consider all available mounted devices for inspection.
   233  
   234  Assertions to be imported must be made available in the auto-import.assert file
   235  in the root of the filesystem.
   236  `)
   237  
   238  func init() {
   239  	cmd := addCommand("auto-import",
   240  		shortAutoImportHelp,
   241  		longAutoImportHelp,
   242  		func() flags.Commander {
   243  			return &cmdAutoImport{}
   244  		}, map[string]string{
   245  			// TRANSLATORS: This should not start with a lowercase letter.
   246  			"mount": i18n.G("Temporarily mount device before inspecting"),
   247  			// TRANSLATORS: This should not start with a lowercase letter.
   248  			"force-classic": i18n.G("Force import on classic systems"),
   249  		}, nil)
   250  	cmd.hidden = true
   251  }
   252  
   253  func (x *cmdAutoImport) autoAddUsers() error {
   254  	cmd := cmdCreateUser{
   255  		clientMixin: x.clientMixin,
   256  		Known:       true,
   257  		Sudoer:      true,
   258  	}
   259  	return cmd.Execute(nil)
   260  }
   261  
   262  func (x *cmdAutoImport) Execute(args []string) error {
   263  	if len(args) > 0 {
   264  		return ErrExtraArgs
   265  	}
   266  
   267  	if release.OnClassic && !x.ForceClassic {
   268  		fmt.Fprintf(Stderr, "auto-import is disabled on classic\n")
   269  		return nil
   270  	}
   271  
   272  	for _, path := range x.Mount {
   273  		// udev adds new /dev/loopX devices on the fly when a
   274  		// loop mount happens and there is no loop device left.
   275  		//
   276  		// We need to ignore these events because otherwise both
   277  		// our mount and the "mount -o loop" fight over the same
   278  		// device and we get nasty errors
   279  		if strings.HasPrefix(path, "/dev/loop") {
   280  			continue
   281  		}
   282  
   283  		mp, err := tryMount(path)
   284  		if err != nil {
   285  			continue // Error was reported. Continue looking.
   286  		}
   287  		defer doUmount(mp)
   288  	}
   289  
   290  	added1, err := autoImportFromSpool(x.client)
   291  	if err != nil {
   292  		return err
   293  	}
   294  
   295  	added2, err := autoImportFromAllMounts(x.client)
   296  	if err != nil {
   297  		return err
   298  	}
   299  
   300  	if added1+added2 > 0 {
   301  		return x.autoAddUsers()
   302  	}
   303  
   304  	return nil
   305  }