github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/osutil/disks/disks.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  	"strconv"
    32  	"strings"
    33  
    34  	"github.com/snapcore/snapd/osutil"
    35  )
    36  
    37  var (
    38  	// for mocking in tests
    39  	devBlockDir = "/sys/dev/block"
    40  
    41  	// this regexp is for the DM_UUID udev property, or equivalently the dm/uuid
    42  	// sysfs entry for a luks2 device mapper volume dynamically created by
    43  	// systemd-cryptsetup when unlocking
    44  	// the actual value that is returned also has "-some-name" appended to this
    45  	// pattern, but we delete that from the string before matching with this
    46  	// regexp to prevent issues like a mapper volume name that has CRYPT-LUKS2-
    47  	// in the name and thus we might accidentally match it
    48  	// see also the comments in DiskFromMountPoint about this value
    49  	luksUUIDPatternRe = regexp.MustCompile(`^CRYPT-LUKS2-([0-9a-f]{32})$`)
    50  )
    51  
    52  // diskFromMountPoint is exposed for mocking from other tests via
    53  // MockMountPointDisksToPartitionMapping, but we can't just assign
    54  // diskFromMountPointImpl to diskFromMountPoint due to signature differences,
    55  // the former returns a *disk, the latter returns a Disk, and as such they can't
    56  // be assigned to each other
    57  var diskFromMountPoint = func(mountpoint string, opts *Options) (Disk, error) {
    58  	return diskFromMountPointImpl(mountpoint, opts)
    59  }
    60  
    61  // Options is a set of options used when querying information about
    62  // partition and disk devices.
    63  type Options struct {
    64  	// IsDecryptedDevice indicates that the mountpoint is referring to a
    65  	// decrypted device.
    66  	IsDecryptedDevice bool
    67  }
    68  
    69  // Disk is a single physical disk device that contains partitions.
    70  // TODO:UC20: add function to get some properties like an associated /dev node
    71  //            for a disk for better user error reporting, i.e. /dev/vda3 is much
    72  //            more helpful than 252:3
    73  type Disk interface {
    74  	// FindMatchingPartitionUUID finds the partition uuid for a partition
    75  	// matching the specified filesystem label on the disk. Note that for
    76  	// non-ascii labels like "Some label", the label will be encoded using
    77  	// \x<hex> for potentially non-safe characters like in "Some\x20Label".
    78  	// If the filesystem label was not found on the disk, and no other errors
    79  	// were encountered, a FilesystemLabelNotFoundError will be returned.
    80  	FindMatchingPartitionUUID(string) (string, error)
    81  
    82  	// MountPointIsFromDisk returns whether the specified mountpoint corresponds
    83  	// to a partition on the disk. Note that this only considers partitions
    84  	// and mountpoints found when the disk was identified with
    85  	// DiskFromMountPoint.
    86  	// TODO:UC20: make this function return what a Disk of where the mount point
    87  	//            is actually from if it is not from the same disk for better
    88  	//            error reporting
    89  	MountPointIsFromDisk(string, *Options) (bool, error)
    90  
    91  	// Dev returns the string "major:minor" number for the disk device.
    92  	Dev() string
    93  
    94  	// HasPartitions returns whether the disk has partitions or not. A physical
    95  	// disk will have partitions, but a mapper device will just be a volume that
    96  	// does not have partitions for example.
    97  	HasPartitions() bool
    98  }
    99  
   100  func parseDeviceMajorMinor(s string) (int, int, error) {
   101  	errMsg := fmt.Errorf("invalid device number format: (expected <int>:<int>)")
   102  	devNums := strings.SplitN(s, ":", 2)
   103  	if len(devNums) != 2 {
   104  		return 0, 0, errMsg
   105  	}
   106  	maj, err := strconv.Atoi(devNums[0])
   107  	if err != nil {
   108  		return 0, 0, errMsg
   109  	}
   110  	min, err := strconv.Atoi(devNums[1])
   111  	if err != nil {
   112  		return 0, 0, errMsg
   113  	}
   114  	return maj, min, nil
   115  }
   116  
   117  var udevadmProperties = func(device string) ([]byte, error) {
   118  	// TODO: maybe combine with gadget interfaces hotplug code where the udev
   119  	// db is parsed?
   120  	cmd := exec.Command("udevadm", "info", "--query", "property", "--name", device)
   121  	return cmd.CombinedOutput()
   122  }
   123  
   124  func udevProperties(device string) (map[string]string, error) {
   125  	out, err := udevadmProperties(device)
   126  	if err != nil {
   127  		return nil, osutil.OutputErr(out, err)
   128  	}
   129  	r := bytes.NewBuffer(out)
   130  
   131  	return parseUdevProperties(r)
   132  }
   133  
   134  func parseUdevProperties(r io.Reader) (map[string]string, error) {
   135  	m := make(map[string]string)
   136  	scanner := bufio.NewScanner(r)
   137  	for scanner.Scan() {
   138  		strs := strings.SplitN(scanner.Text(), "=", 2)
   139  		if len(strs) != 2 {
   140  			// bad udev output?
   141  			continue
   142  		}
   143  		m[strs[0]] = strs[1]
   144  	}
   145  
   146  	return m, scanner.Err()
   147  }
   148  
   149  // DiskFromMountPoint finds a matching Disk for the specified mount point.
   150  func DiskFromMountPoint(mountpoint string, opts *Options) (Disk, error) {
   151  	// call the unexported version that may be mocked by tests
   152  	return diskFromMountPoint(mountpoint, opts)
   153  }
   154  
   155  type disk struct {
   156  	major int
   157  	minor int
   158  	// fsLabelToPartUUID is a map of filesystem label -> partition uuid for now
   159  	// eventually this may be expanded to be more generally useful
   160  	fsLabelToPartUUID map[string]string
   161  
   162  	// whether the disk device has partitions, and thus is of type "disk", or
   163  	// whether the disk device is a volume that is not a physical disk
   164  	hasPartitions bool
   165  }
   166  
   167  // diskFromMountPointImpl returns a Disk for the underlying mount source of the
   168  // specified mount point. For mount points which have sources that are not
   169  // partitions, and thus are a part of a disk, the returned Disk refers to the
   170  // volume/disk of the mount point itself.
   171  func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) {
   172  	// first get the mount entry for the mountpoint
   173  	mounts, err := osutil.LoadMountInfo()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	var d *disk
   178  	var partMountPointSource string
   179  	// loop over the mount entries in reverse order to prevent shadowing of a
   180  	// particular mount on top of another one
   181  	for i := len(mounts) - 1; i >= 0; i-- {
   182  		if mounts[i].MountDir == mountpoint {
   183  			d = &disk{
   184  				major: mounts[i].DevMajor,
   185  				minor: mounts[i].DevMinor,
   186  			}
   187  			partMountPointSource = mounts[i].MountSource
   188  			break
   189  		}
   190  	}
   191  	if d == nil {
   192  		return nil, fmt.Errorf("cannot find mountpoint %q", mountpoint)
   193  	}
   194  
   195  	// now we have the partition for this mountpoint, we need to tie that back
   196  	// to a disk with a major minor, so query udev with the mount source path
   197  	// of the mountpoint for properties
   198  	props, err := udevProperties(partMountPointSource)
   199  	if err != nil && props == nil {
   200  		// only fail here if props is nil, if it's available we validate it
   201  		// below
   202  		return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err)
   203  	}
   204  
   205  	if opts != nil && opts.IsDecryptedDevice {
   206  		// verify that the mount point is indeed a mapper device, it should:
   207  		// 1. have DEVTYPE == disk from udev
   208  		// 2. have dm files in the sysfs entry for the maj:min of the device
   209  		if props["DEVTYPE"] != "disk" {
   210  			// not a decrypted device
   211  			return nil, fmt.Errorf("mountpoint source %s is not a decrypted device: devtype is not disk (is %s)", partMountPointSource, props["DEVTYPE"])
   212  		}
   213  
   214  		// TODO:UC20: currently, we effectively parse the DM_UUID env variable
   215  		//            that is set for the mapper device volume, but doing so is
   216  		//            actually wrong, since the value of DM_UUID is an
   217  		//            implementation detail that depends on the subsystem
   218  		//            "owner" of the device such that the prefix is considered
   219  		//            the owner and the suffix is private data owned by the
   220  		//            subsystem. In our case, in UC20 initramfs, we have the
   221  		//            device "owned" by systemd-cryptsetup, so we should ideally
   222  		//            parse that the first part of DM_UUID matches CRYPT- and
   223  		//            then use `cryptsetup status` (since CRYPT indicates it is
   224  		//            "owned" by cryptsetup) to get more information on the
   225  		//            device sufficient for our purposes to find the encrypted
   226  		//            device/partition underneath the mapper.
   227  		//            However we don't currently have cryptsetup in the initrd,
   228  		//            so we can't do that yet :-(
   229  
   230  		// TODO:UC20: these files are also likely readable through udev env
   231  		//            properties, but it's unclear if reading there is reliable
   232  		//            or not, given that these variables have been observed to
   233  		//            be missing from the initrd previously, and are not
   234  		//            available at all during userspace on UC20 for some reason
   235  		errFmt := "mountpoint source %s is not a decrypted device: could not read device mapper metadata: %v"
   236  		dmUUID, err := ioutil.ReadFile(filepath.Join(devBlockDir, d.Dev(), "dm", "uuid"))
   237  		if err != nil {
   238  			return nil, fmt.Errorf(errFmt, partMountPointSource, err)
   239  		}
   240  
   241  		dmName, err := ioutil.ReadFile(filepath.Join(devBlockDir, d.Dev(), "dm", "name"))
   242  		if err != nil {
   243  			return nil, fmt.Errorf(errFmt, partMountPointSource, err)
   244  		}
   245  
   246  		// trim the suffix of the dm name from the dm uuid to safely match the
   247  		// regex - the dm uuid contains the dm name, and the dm name is user
   248  		// controlled, so we want to remove that and just use the luks pattern
   249  		// to match the device uuid
   250  		// we are extra safe here since the dm name could be hypothetically user
   251  		// controlled via an external USB disk with LVM partition names, etc.
   252  		dmUUIDSafe := bytes.TrimSuffix(
   253  			bytes.TrimSpace(dmUUID),
   254  			append([]byte("-"), bytes.TrimSpace(dmName)...),
   255  		)
   256  		matches := luksUUIDPatternRe.FindSubmatch(dmUUIDSafe)
   257  		if len(matches) != 2 {
   258  			// the format of the uuid is different - different luks version
   259  			// maybe?
   260  			return nil, fmt.Errorf("cannot verify disk: partition %s does not have a valid luks uuid format: %s", d.Dev(), dmUUIDSafe)
   261  		}
   262  
   263  		// the uuid is the first and only submatch, but it is not in the same
   264  		// format exactly as we want to use, namely it is missing all of the "-"
   265  		// characters in a typical uuid, i.e. it is of the form:
   266  		// ae6e79de00a9406f80ee64ba7c1966bb but we want it to be like:
   267  		// ae6e79de-00a9-406f-80ee-64ba7c1966bb so we need to add in 4 "-"
   268  		// characters
   269  		compactUUID := string(matches[1])
   270  		canonicalUUID := fmt.Sprintf(
   271  			"%s-%s-%s-%s-%s",
   272  			compactUUID[0:8],
   273  			compactUUID[8:12],
   274  			compactUUID[12:16],
   275  			compactUUID[16:20],
   276  			compactUUID[20:],
   277  		)
   278  
   279  		// now finally, we need to use this uuid, which is the device uuid of
   280  		// the actual physical encrypted partition to get the path, which will
   281  		// be something like /dev/vda4, etc.
   282  		byUUIDPath := filepath.Join("/dev/disk/by-uuid", canonicalUUID)
   283  		props, err = udevProperties(byUUIDPath)
   284  		if err != nil {
   285  			return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", byUUIDPath, err)
   286  		}
   287  	}
   288  
   289  	// ID_PART_ENTRY_DISK will give us the major and minor of the disk that this
   290  	// partition originated from
   291  	if majorMinor, ok := props["ID_PART_ENTRY_DISK"]; ok {
   292  		maj, min, err := parseDeviceMajorMinor(majorMinor)
   293  		if err != nil {
   294  			// bad udev output?
   295  			return nil, fmt.Errorf("cannot find disk for partition %s, bad udev output: %v", partMountPointSource, err)
   296  		}
   297  		d.major = maj
   298  		d.minor = min
   299  
   300  		// since the mountpoint device has a disk, the mountpoint source itself
   301  		// must be a partition from a disk, thus the disk has partitions
   302  		d.hasPartitions = true
   303  		return d, nil
   304  	}
   305  
   306  	// if we don't have ID_PART_ENTRY_DISK, the partition is probably a volume
   307  	// or other non-physical disk, so confirm that DEVTYPE == disk and return
   308  	// the maj/min for it
   309  	if devType, ok := props["DEVTYPE"]; ok {
   310  		if devType == "disk" {
   311  			return d, nil
   312  		}
   313  		// unclear what other DEVTYPE's we should support for this function
   314  		return nil, fmt.Errorf("unsupported DEVTYPE %q for mount point source %s", devType, partMountPointSource)
   315  	}
   316  
   317  	return nil, fmt.Errorf("cannot find disk for partition %s, incomplete udev output", partMountPointSource)
   318  }
   319  
   320  // FilesystemLabelNotFoundError is an error where the specified label was not
   321  // found on the disk.
   322  type FilesystemLabelNotFoundError struct {
   323  	Label string
   324  }
   325  
   326  var (
   327  	_ = error(FilesystemLabelNotFoundError{})
   328  )
   329  
   330  func (e FilesystemLabelNotFoundError) Error() string {
   331  	return fmt.Sprintf("filesystem label %q not found", e.Label)
   332  }
   333  
   334  func (d *disk) FindMatchingPartitionUUID(label string) (string, error) {
   335  	encodedLabel := BlkIDEncodeLabel(label)
   336  	// if we haven't found the partitions for this disk yet, do that now
   337  	if d.fsLabelToPartUUID == nil {
   338  		d.fsLabelToPartUUID = make(map[string]string)
   339  		// step 1. find all devices with a matching major number
   340  		// step 2. start at the major + minor device for the disk, and iterate over
   341  		//         all devices that have a partition attribute, starting with the
   342  		//         device with major same as disk and minor equal to disk minor + 1
   343  		// step 3. if we hit a device that does not have a partition attribute, then
   344  		//         we hit another disk, and shall stop searching
   345  
   346  		// note that this code assumes that all contiguous major / minor devices
   347  		// belong to the same physical device, even with MBR and
   348  		// logical/extended partition nodes jumping to i.e. /dev/sd*5
   349  
   350  		// start with the minor + 1, since the major + minor of the disk we have
   351  		// itself is not a partition
   352  		currentMinor := d.minor
   353  		for {
   354  			currentMinor++
   355  			partMajMin := fmt.Sprintf("%d:%d", d.major, currentMinor)
   356  			props, err := udevProperties(filepath.Join("/dev/block", partMajMin))
   357  			if err != nil && strings.Contains(err.Error(), "Unknown device") {
   358  				// the device doesn't exist, we hit the end of the disk
   359  				break
   360  			} else if err != nil {
   361  				// some other error trying to get udev properties, we should fail
   362  				return "", fmt.Errorf("cannot get udev properties for partition %s: %v", partMajMin, err)
   363  			}
   364  
   365  			if props["DEVTYPE"] != "partition" {
   366  				// we ran into another disk, break out
   367  				break
   368  			}
   369  
   370  			// TODO: maybe save ID_PART_ENTRY_NAME here too, which is the name
   371  			//       of the partition. this may be useful if this function gets
   372  			//       used in the gadget update code
   373  			fsLabelEnc := props["ID_FS_LABEL_ENC"]
   374  			if fsLabelEnc == "" {
   375  				// this partition does not have a filesystem, and thus doesn't
   376  				// have a filesystem label - this is not fatal, i.e. the
   377  				// bios-boot partition does not have a filesystem label but it
   378  				// is the first structure and so we should just skip it
   379  				continue
   380  			}
   381  
   382  			partuuid := props["ID_PART_ENTRY_UUID"]
   383  			if partuuid == "" {
   384  				return "", fmt.Errorf("cannot get udev properties for partition %s, missing udev property \"ID_PART_ENTRY_UUID\"", partMajMin)
   385  			}
   386  
   387  			// we always overwrite the fsLabelEnc with the last one, this has
   388  			// the result that the last partition with a given filesystem label
   389  			// will be set/found
   390  			// this matches what udev does with the symlinks in /dev
   391  			d.fsLabelToPartUUID[fsLabelEnc] = partuuid
   392  		}
   393  	}
   394  
   395  	// if we didn't find any partitions from above then return an error
   396  	if len(d.fsLabelToPartUUID) == 0 {
   397  		return "", fmt.Errorf("no partitions found for disk %s", d.Dev())
   398  	}
   399  
   400  	if partuuid, ok := d.fsLabelToPartUUID[encodedLabel]; ok {
   401  		return partuuid, nil
   402  	}
   403  
   404  	return "", FilesystemLabelNotFoundError{Label: label}
   405  }
   406  
   407  func (d *disk) MountPointIsFromDisk(mountpoint string, opts *Options) (bool, error) {
   408  	d2, err := diskFromMountPointImpl(mountpoint, opts)
   409  	if err != nil {
   410  		return false, err
   411  	}
   412  
   413  	// compare if the major/minor devices are the same and if both devices have
   414  	// partitions
   415  	return d.major == d2.major &&
   416  			d.minor == d2.minor &&
   417  			d.hasPartitions == d2.hasPartitions,
   418  		nil
   419  }
   420  
   421  func (d *disk) Dev() string {
   422  	return fmt.Sprintf("%d:%d", d.major, d.minor)
   423  }
   424  
   425  func (d *disk) HasPartitions() bool {
   426  	return d.hasPartitions
   427  }