github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/disks/disks_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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 disks
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"os/exec"
    29  	"path/filepath"
    30  	"regexp"
    31  	"sort"
    32  	"strconv"
    33  	"strings"
    34  
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/osutil"
    37  )
    38  
    39  var (
    40  	// this regexp is for the DM_UUID udev property, or equivalently the dm/uuid
    41  	// sysfs entry for a luks2 device mapper volume dynamically created by
    42  	// systemd-cryptsetup when unlocking
    43  	// the actual value that is returned also has "-some-name" appended to this
    44  	// pattern, but we delete that from the string before matching with this
    45  	// regexp to prevent issues like a mapper volume name that has CRYPT-LUKS2-
    46  	// in the name and thus we might accidentally match it
    47  	// see also the comments in DiskFromMountPoint about this value
    48  	luksUUIDPatternRe = regexp.MustCompile(`^CRYPT-LUKS2-([0-9a-f]{32})$`)
    49  )
    50  
    51  // diskFromMountPoint is exposed for mocking from other tests via
    52  // MockMountPointDisksToPartitionMapping, but we can't just assign
    53  // diskFromMountPointImpl to diskFromMountPoint due to signature differences,
    54  // the former returns a *disk, the latter returns a Disk, and as such they can't
    55  // be assigned to each other
    56  var diskFromMountPoint = func(mountpoint string, opts *Options) (Disk, error) {
    57  	return diskFromMountPointImpl(mountpoint, opts)
    58  }
    59  
    60  func parseDeviceMajorMinor(s string) (int, int, error) {
    61  	errMsg := fmt.Errorf("invalid device number format: (expected <int>:<int>)")
    62  	devNums := strings.SplitN(s, ":", 2)
    63  	if len(devNums) != 2 {
    64  		return 0, 0, errMsg
    65  	}
    66  	maj, err := strconv.Atoi(devNums[0])
    67  	if err != nil {
    68  		return 0, 0, errMsg
    69  	}
    70  	min, err := strconv.Atoi(devNums[1])
    71  	if err != nil {
    72  		return 0, 0, errMsg
    73  	}
    74  	return maj, min, nil
    75  }
    76  
    77  var udevadmProperties = func(device string) ([]byte, error) {
    78  	// TODO: maybe combine with gadget interfaces hotplug code where the udev
    79  	// db is parsed?
    80  	cmd := exec.Command("udevadm", "info", "--query", "property", "--name", device)
    81  	return cmd.CombinedOutput()
    82  }
    83  
    84  func udevProperties(device string) (map[string]string, error) {
    85  	out, err := udevadmProperties(device)
    86  	if err != nil {
    87  		return nil, osutil.OutputErr(out, err)
    88  	}
    89  	r := bytes.NewBuffer(out)
    90  
    91  	return parseUdevProperties(r)
    92  }
    93  
    94  func parseUdevProperties(r io.Reader) (map[string]string, error) {
    95  	m := make(map[string]string)
    96  	scanner := bufio.NewScanner(r)
    97  	for scanner.Scan() {
    98  		strs := strings.SplitN(scanner.Text(), "=", 2)
    99  		if len(strs) != 2 {
   100  			// bad udev output?
   101  			continue
   102  		}
   103  		m[strs[0]] = strs[1]
   104  	}
   105  
   106  	return m, scanner.Err()
   107  }
   108  
   109  // DiskFromDeviceName finds a matching Disk using the specified name, such as
   110  // vda, or mmcblk0, etc.
   111  func DiskFromDeviceName(deviceName string) (Disk, error) {
   112  	return diskFromDeviceName(deviceName)
   113  }
   114  
   115  // diskFromDeviceName is exposed for mocking from other tests via
   116  // MockDeviceNameDisksToPartitionMapping.
   117  var diskFromDeviceName = func(deviceName string) (Disk, error) {
   118  	// query for the disk props using udev
   119  	props, err := udevProperties(deviceName)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	major, err := strconv.Atoi(props["MAJOR"])
   125  	if err != nil {
   126  		return nil, fmt.Errorf("cannot find disk with name %q: malformed udev output", deviceName)
   127  	}
   128  	minor, err := strconv.Atoi(props["MINOR"])
   129  	if err != nil {
   130  		return nil, fmt.Errorf("cannot find disk with name %q: malformed udev output", deviceName)
   131  	}
   132  
   133  	// ensure that the device has DEVTYPE=disk, if not then we were not given a
   134  	// disk name
   135  	devType := props["DEVTYPE"]
   136  	if devType != "disk" {
   137  		return nil, fmt.Errorf("device %q is not a disk, it has DEVTYPE of %q", deviceName, devType)
   138  	}
   139  
   140  	// TODO: should we try to introspect the device more to find out if it has
   141  	// partitions? we don't currently need that information for how we use
   142  	// DiskFromName but it might be useful eventually
   143  
   144  	return &disk{
   145  		major: major,
   146  		minor: minor,
   147  	}, nil
   148  }
   149  
   150  // DiskFromMountPoint finds a matching Disk for the specified mount point.
   151  func DiskFromMountPoint(mountpoint string, opts *Options) (Disk, error) {
   152  	// call the unexported version that may be mocked by tests
   153  	return diskFromMountPoint(mountpoint, opts)
   154  }
   155  
   156  type partition struct {
   157  	fsLabel   string
   158  	partLabel string
   159  	partUUID  string
   160  }
   161  
   162  type disk struct {
   163  	major int
   164  	minor int
   165  	// partitions is the set of discovered partitions for the disk, each
   166  	// partition must have a partition uuid, but may or may not have either a
   167  	// partition label or a filesystem label
   168  	partitions []partition
   169  
   170  	// whether the disk device has partitions, and thus is of type "disk", or
   171  	// whether the disk device is a volume that is not a physical disk
   172  	hasPartitions bool
   173  }
   174  
   175  // diskFromMountPointImpl returns a Disk for the underlying mount source of the
   176  // specified mount point. For mount points which have sources that are not
   177  // partitions, and thus are a part of a disk, the returned Disk refers to the
   178  // volume/disk of the mount point itself.
   179  func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) {
   180  	// first get the mount entry for the mountpoint
   181  	mounts, err := osutil.LoadMountInfo()
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	var d *disk
   186  	var partMountPointSource string
   187  	// loop over the mount entries in reverse order to prevent shadowing of a
   188  	// particular mount on top of another one
   189  	for i := len(mounts) - 1; i >= 0; i-- {
   190  		if mounts[i].MountDir == mountpoint {
   191  			d = &disk{
   192  				major: mounts[i].DevMajor,
   193  				minor: mounts[i].DevMinor,
   194  			}
   195  			partMountPointSource = mounts[i].MountSource
   196  			break
   197  		}
   198  	}
   199  	if d == nil {
   200  		return nil, fmt.Errorf("cannot find mountpoint %q", mountpoint)
   201  	}
   202  
   203  	// now we have the partition for this mountpoint, we need to tie that back
   204  	// to a disk with a major minor, so query udev with the mount source path
   205  	// of the mountpoint for properties
   206  	props, err := udevProperties(partMountPointSource)
   207  	if err != nil && props == nil {
   208  		// only fail here if props is nil, if it's available we validate it
   209  		// below
   210  		return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err)
   211  	}
   212  
   213  	if opts != nil && opts.IsDecryptedDevice {
   214  		// verify that the mount point is indeed a mapper device, it should:
   215  		// 1. have DEVTYPE == disk from udev
   216  		// 2. have dm files in the sysfs entry for the maj:min of the device
   217  		if props["DEVTYPE"] != "disk" {
   218  			// not a decrypted device
   219  			return nil, fmt.Errorf("mountpoint source %s is not a decrypted device: devtype is not disk (is %s)", partMountPointSource, props["DEVTYPE"])
   220  		}
   221  
   222  		// TODO:UC20: currently, we effectively parse the DM_UUID env variable
   223  		//            that is set for the mapper device volume, but doing so is
   224  		//            actually wrong, since the value of DM_UUID is an
   225  		//            implementation detail that depends on the subsystem
   226  		//            "owner" of the device such that the prefix is considered
   227  		//            the owner and the suffix is private data owned by the
   228  		//            subsystem. In our case, in UC20 initramfs, we have the
   229  		//            device "owned" by systemd-cryptsetup, so we should ideally
   230  		//            parse that the first part of DM_UUID matches CRYPT- and
   231  		//            then use `cryptsetup status` (since CRYPT indicates it is
   232  		//            "owned" by cryptsetup) to get more information on the
   233  		//            device sufficient for our purposes to find the encrypted
   234  		//            device/partition underneath the mapper.
   235  		//            However we don't currently have cryptsetup in the initrd,
   236  		//            so we can't do that yet :-(
   237  
   238  		// TODO:UC20: these files are also likely readable through udev env
   239  		//            properties, but it's unclear if reading there is reliable
   240  		//            or not, given that these variables have been observed to
   241  		//            be missing from the initrd previously, and are not
   242  		//            available at all during userspace on UC20 for some reason
   243  		errFmt := "mountpoint source %s is not a decrypted device: could not read device mapper metadata: %v"
   244  
   245  		dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", d.Dev(), "dm")
   246  		dmUUID, err := ioutil.ReadFile(filepath.Join(dmDir, "uuid"))
   247  		if err != nil {
   248  			return nil, fmt.Errorf(errFmt, partMountPointSource, err)
   249  		}
   250  
   251  		dmName, err := ioutil.ReadFile(filepath.Join(dmDir, "name"))
   252  		if err != nil {
   253  			return nil, fmt.Errorf(errFmt, partMountPointSource, err)
   254  		}
   255  
   256  		// trim the suffix of the dm name from the dm uuid to safely match the
   257  		// regex - the dm uuid contains the dm name, and the dm name is user
   258  		// controlled, so we want to remove that and just use the luks pattern
   259  		// to match the device uuid
   260  		// we are extra safe here since the dm name could be hypothetically user
   261  		// controlled via an external USB disk with LVM partition names, etc.
   262  		dmUUIDSafe := bytes.TrimSuffix(
   263  			bytes.TrimSpace(dmUUID),
   264  			append([]byte("-"), bytes.TrimSpace(dmName)...),
   265  		)
   266  		matches := luksUUIDPatternRe.FindSubmatch(dmUUIDSafe)
   267  		if len(matches) != 2 {
   268  			// the format of the uuid is different - different luks version
   269  			// maybe?
   270  			return nil, fmt.Errorf("cannot verify disk: partition %s does not have a valid luks uuid format: %s", d.Dev(), dmUUIDSafe)
   271  		}
   272  
   273  		// the uuid is the first and only submatch, but it is not in the same
   274  		// format exactly as we want to use, namely it is missing all of the "-"
   275  		// characters in a typical uuid, i.e. it is of the form:
   276  		// ae6e79de00a9406f80ee64ba7c1966bb but we want it to be like:
   277  		// ae6e79de-00a9-406f-80ee-64ba7c1966bb so we need to add in 4 "-"
   278  		// characters
   279  		compactUUID := string(matches[1])
   280  		canonicalUUID := fmt.Sprintf(
   281  			"%s-%s-%s-%s-%s",
   282  			compactUUID[0:8],
   283  			compactUUID[8:12],
   284  			compactUUID[12:16],
   285  			compactUUID[16:20],
   286  			compactUUID[20:],
   287  		)
   288  
   289  		// now finally, we need to use this uuid, which is the device uuid of
   290  		// the actual physical encrypted partition to get the path, which will
   291  		// be something like /dev/vda4, etc.
   292  		byUUIDPath := filepath.Join("/dev/disk/by-uuid", canonicalUUID)
   293  		props, err = udevProperties(byUUIDPath)
   294  		if err != nil {
   295  			return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", byUUIDPath, err)
   296  		}
   297  	}
   298  
   299  	// ID_PART_ENTRY_DISK will give us the major and minor of the disk that this
   300  	// partition originated from
   301  	if majorMinor, ok := props["ID_PART_ENTRY_DISK"]; ok {
   302  		maj, min, err := parseDeviceMajorMinor(majorMinor)
   303  		if err != nil {
   304  			// bad udev output?
   305  			return nil, fmt.Errorf("cannot find disk for partition %s, bad udev output: %v", partMountPointSource, err)
   306  		}
   307  		d.major = maj
   308  		d.minor = min
   309  
   310  		// since the mountpoint device has a disk, the mountpoint source itself
   311  		// must be a partition from a disk, thus the disk has partitions
   312  		d.hasPartitions = true
   313  		return d, nil
   314  	}
   315  
   316  	// if we don't have ID_PART_ENTRY_DISK, the partition is probably a volume
   317  	// or other non-physical disk, so confirm that DEVTYPE == disk and return
   318  	// the maj/min for it
   319  	if devType, ok := props["DEVTYPE"]; ok {
   320  		if devType == "disk" {
   321  			return d, nil
   322  		}
   323  		// unclear what other DEVTYPE's we should support for this function
   324  		return nil, fmt.Errorf("unsupported DEVTYPE %q for mount point source %s", devType, partMountPointSource)
   325  	}
   326  
   327  	return nil, fmt.Errorf("cannot find disk for partition %s, incomplete udev output", partMountPointSource)
   328  }
   329  
   330  func (d *disk) populatePartitions() error {
   331  	if d.partitions == nil {
   332  		d.partitions = []partition{}
   333  
   334  		// step 1. find the devpath for the disk, then glob for matching
   335  		//         devices using the devname in that sysfs directory
   336  		// step 2. iterate over all those devices and save all the ones that are
   337  		//         partitions using the partition sysfs file
   338  		// step 3. for all partition devices found, query udev to get the labels
   339  		//         of the partition and filesystem as well as the partition uuid
   340  		//         and save for later
   341  
   342  		udevProps, err := udevProperties(filepath.Join("/dev/block", d.Dev()))
   343  		if err != nil {
   344  			return err
   345  		}
   346  
   347  		// get the base device name
   348  		devName := udevProps["DEVNAME"]
   349  		if devName == "" {
   350  			return fmt.Errorf("cannot get udev properties for device %s, missing udev property \"DEVNAME\"", d.Dev())
   351  		}
   352  		// the DEVNAME as returned by udev includes the /dev/mmcblk0 path, we
   353  		// just want mmcblk0 for example
   354  		devName = filepath.Base(devName)
   355  
   356  		// get the device path in sysfs
   357  		devPath := udevProps["DEVPATH"]
   358  		if devPath == "" {
   359  			return fmt.Errorf("cannot get udev properties for device %s, missing udev property \"DEVPATH\"", d.Dev())
   360  		}
   361  
   362  		// glob for /sys/${devPath}/${devName}*
   363  		paths, err := filepath.Glob(filepath.Join(dirs.SysfsDir, devPath, devName+"*"))
   364  		if err != nil {
   365  			return fmt.Errorf("internal error getting udev properties for device %s: %v", err, d.Dev())
   366  		}
   367  
   368  		// Glob does not sort, so sort manually to have consistent tests
   369  		sort.Strings(paths)
   370  
   371  		for _, path := range paths {
   372  			part := partition{}
   373  
   374  			// check if this device is a partition - note that the mere
   375  			// existence of this file is sufficient to indicate that it is a
   376  			// partition, the file is the partition number of the device, it
   377  			// will be absent for pseudo sub-devices, such as the
   378  			// /dev/mmcblk0boot0 disk device on the dragonboard which exists
   379  			// under the /dev/mmcblk0 disk, but is not a partition and is
   380  			// instead a proper disk
   381  			_, err := ioutil.ReadFile(filepath.Join(path, "partition"))
   382  			if err != nil {
   383  				continue
   384  			}
   385  
   386  			// then the device is a partition, get the udev props for it
   387  			partDev := filepath.Base(path)
   388  			udevProps, err := udevProperties(partDev)
   389  			if err != nil {
   390  				continue
   391  			}
   392  
   393  			// we should always have the partition uuid, and we may not have
   394  			// either the partition label or the filesystem label, on GPT disks
   395  			// the partition label is optional, and may or may not have a
   396  			// filesystem on the partition, on MBR we will never have a
   397  			// partition label, and we also may or may not have a filesystem on
   398  			// the partition
   399  			part.partUUID = udevProps["ID_PART_ENTRY_UUID"]
   400  			if part.partUUID == "" {
   401  				return fmt.Errorf("cannot get udev properties for device %s (a partition of %s), missing udev property \"ID_PART_ENTRY_UUID\"", partDev, d.Dev())
   402  			}
   403  
   404  			// on MBR disks we may not have a partition label, so this may be
   405  			// the empty string. Note that this value is encoded similarly to
   406  			// libblkid and should be compared with normal Go strings that are
   407  			// encoded using BlkIDEncodeLabel.
   408  			part.partLabel = udevProps["ID_PART_ENTRY_NAME"]
   409  
   410  			// a partition doesn't need to have a filesystem, and such may not
   411  			// have a filesystem label; the bios-boot partition in the amd64 pc
   412  			// gadget is such an example of a partition GPT that does not have a
   413  			// filesystem.
   414  			// Note that this value is also encoded similarly to
   415  			// ID_PART_ENTRY_NAME and thus should only be compared with normal
   416  			// Go strings that are encoded with BlkIDEncodeLabel.
   417  			part.fsLabel = udevProps["ID_FS_LABEL_ENC"]
   418  
   419  			// prepend the partition to the front, this has the effect that if
   420  			// two partitions have the same label (either filesystem or
   421  			// partition though it is unclear whether you could actually in
   422  			// practice create a disk partitioning scheme with the same
   423  			// partition label for multiple partitions), then the one we look at
   424  			// last while populating will be the one that the Find*()
   425  			// functions locate first while iterating over the disk's partitions
   426  			// this behavior matches what udev does
   427  			// TODO: perhaps we should just explicitly not support disks with
   428  			// non-unique filesystem labels or non-unique partition labels (or
   429  			// even non-unique partition uuids)? then we would just error if we
   430  			// encounter a duplicated value for a partition
   431  			d.partitions = append([]partition{part}, d.partitions...)
   432  		}
   433  	}
   434  
   435  	// if we didn't find any partitions from above then return an error, this is
   436  	// because all disks we search for partitions are expected to have some
   437  	// partitions
   438  	if len(d.partitions) == 0 {
   439  		return fmt.Errorf("no partitions found for disk %s", d.Dev())
   440  	}
   441  
   442  	return nil
   443  }
   444  
   445  func (d *disk) FindMatchingPartitionUUIDWithPartLabel(label string) (string, error) {
   446  	// always encode the label
   447  	encodedLabel := BlkIDEncodeLabel(label)
   448  
   449  	if err := d.populatePartitions(); err != nil {
   450  		return "", err
   451  	}
   452  
   453  	for _, p := range d.partitions {
   454  		if p.partLabel == encodedLabel {
   455  			return p.partUUID, nil
   456  		}
   457  	}
   458  
   459  	return "", PartitionNotFoundError{
   460  		SearchType:  "partition-label",
   461  		SearchQuery: label,
   462  	}
   463  }
   464  
   465  func (d *disk) FindMatchingPartitionUUIDWithFsLabel(label string) (string, error) {
   466  	// always encode the label
   467  	encodedLabel := BlkIDEncodeLabel(label)
   468  
   469  	if err := d.populatePartitions(); err != nil {
   470  		return "", err
   471  	}
   472  
   473  	for _, p := range d.partitions {
   474  		if p.fsLabel == encodedLabel {
   475  			return p.partUUID, nil
   476  		}
   477  	}
   478  
   479  	return "", PartitionNotFoundError{
   480  		SearchType:  "filesystem-label",
   481  		SearchQuery: label,
   482  	}
   483  }
   484  
   485  func (d *disk) MountPointIsFromDisk(mountpoint string, opts *Options) (bool, error) {
   486  	d2, err := diskFromMountPointImpl(mountpoint, opts)
   487  	if err != nil {
   488  		return false, err
   489  	}
   490  
   491  	// compare if the major/minor devices are the same and if both devices have
   492  	// partitions
   493  	return d.major == d2.major &&
   494  			d.minor == d2.minor &&
   495  			d.hasPartitions == d2.hasPartitions,
   496  		nil
   497  }
   498  
   499  func (d *disk) Dev() string {
   500  	return fmt.Sprintf("%d:%d", d.major, d.minor)
   501  }
   502  
   503  func (d *disk) HasPartitions() bool {
   504  	// TODO: instead of saving this value when we create/discover the disk, we
   505  	//       could instead populate the partitions here and then return whether
   506  	//       d.partitions is empty or not
   507  	return d.hasPartitions
   508  }