github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/device_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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 gadget
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/gadget/quantity"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/osutil/disks"
    33  )
    34  
    35  var ErrDeviceNotFound = errors.New("device not found")
    36  var ErrMountNotFound = errors.New("mount point not found")
    37  var ErrNoFilesystemDefined = errors.New("no filesystem defined")
    38  
    39  var evalSymlinks = filepath.EvalSymlinks
    40  
    41  // FindDeviceForStructure attempts to find an existing block device matching
    42  // given volume structure, by inspecting its name and, optionally, the
    43  // filesystem label. Assumes that the host's udev has set up device symlinks
    44  // correctly.
    45  func FindDeviceForStructure(ps *LaidOutStructure) (string, error) {
    46  	var candidates []string
    47  
    48  	if ps.Name != "" {
    49  		byPartlabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel/", disks.BlkIDEncodeLabel(ps.Name))
    50  		candidates = append(candidates, byPartlabel)
    51  	}
    52  	if ps.HasFilesystem() {
    53  		fsLabel := ps.Label
    54  		if fsLabel == "" && ps.Name != "" {
    55  			// when image is built and the structure has no
    56  			// filesystem label, the structure name will be used by
    57  			// default as the label
    58  			fsLabel = ps.Name
    59  		}
    60  		if fsLabel != "" {
    61  			byFsLabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-label/", disks.BlkIDEncodeLabel(fsLabel))
    62  			candidates = append(candidates, byFsLabel)
    63  		}
    64  	}
    65  
    66  	var found string
    67  	var match string
    68  	for _, candidate := range candidates {
    69  		if !osutil.FileExists(candidate) {
    70  			continue
    71  		}
    72  		if !osutil.IsSymlink(candidate) {
    73  			// /dev/disk/by-label/* and /dev/disk/by-partlabel/* are
    74  			// expected to be symlink
    75  			return "", fmt.Errorf("candidate %v is not a symlink", candidate)
    76  		}
    77  		target, err := evalSymlinks(candidate)
    78  		if err != nil {
    79  			return "", fmt.Errorf("cannot read device link: %v", err)
    80  		}
    81  		if found != "" && target != found {
    82  			// partition and filesystem label links point to
    83  			// different devices
    84  			return "", fmt.Errorf("conflicting device match, %q points to %q, previous match %q points to %q",
    85  				candidate, target, match, found)
    86  		}
    87  		found = target
    88  		match = candidate
    89  	}
    90  
    91  	if found == "" {
    92  		return "", ErrDeviceNotFound
    93  	}
    94  
    95  	return found, nil
    96  }
    97  
    98  // findDeviceForStructureWithFallback attempts to find an existing block device
    99  // partition containing given non-filesystem volume structure, by inspecting the
   100  // structure's name.
   101  //
   102  // Should there be no match, attempts to find the block device corresponding to
   103  // the volume enclosing the structure under the following conditions:
   104  // - the structure has no filesystem
   105  // - and the structure is of type: bare (no partition table entry)
   106  // - or the structure has no name, but a partition table entry (hence no label
   107  //   by which we could find it)
   108  //
   109  // The fallback mechanism uses the fact that Core devices always have a mount at
   110  // /writable. The system is booted from the parent of the device mounted at
   111  // /writable.
   112  //
   113  // Returns the device name and an offset at which the structure content starts
   114  // within the device or an error.
   115  func findDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs quantity.Offset, err error) {
   116  	if ps.HasFilesystem() {
   117  		return "", 0, fmt.Errorf("internal error: cannot use with filesystem structures")
   118  	}
   119  
   120  	dev, err = FindDeviceForStructure(ps)
   121  	if err == nil {
   122  		// found exact device representing this structure, thus the
   123  		// structure starts at 0 offset within the device
   124  		return dev, 0, nil
   125  	}
   126  	if err != ErrDeviceNotFound {
   127  		// error out on other errors
   128  		return "", 0, err
   129  	}
   130  	if err == ErrDeviceNotFound && ps.IsPartition() && ps.Name != "" {
   131  		// structures with partition table entry and a name must have
   132  		// been located already
   133  		return "", 0, err
   134  	}
   135  
   136  	// we're left with structures that have no partition table entry, or
   137  	// have a partition but no name that could be used to find them
   138  
   139  	dev, err = findParentDeviceWithWritableFallback()
   140  	if err != nil {
   141  		return "", 0, err
   142  	}
   143  	// start offset is calculated as an absolute position within the volume
   144  	return dev, ps.StartOffset, nil
   145  }
   146  
   147  // findMountPointForStructure locates a mount point of a device that matches
   148  // given structure. The structure must have a filesystem defined, otherwise an
   149  // error is raised.
   150  func findMountPointForStructure(ps *LaidOutStructure) (string, error) {
   151  	if !ps.HasFilesystem() {
   152  		return "", ErrNoFilesystemDefined
   153  	}
   154  
   155  	devpath, err := FindDeviceForStructure(ps)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  
   160  	var mountPoint string
   161  	mountInfo, err := osutil.LoadMountInfo()
   162  	if err != nil {
   163  		return "", fmt.Errorf("cannot read mount info: %v", err)
   164  	}
   165  	for _, entry := range mountInfo {
   166  		if entry.Root != "/" {
   167  			// only interested at the location where root of the
   168  			// structure filesystem is mounted
   169  			continue
   170  		}
   171  		if entry.MountSource == devpath && entry.FsType == ps.Filesystem {
   172  			mountPoint = entry.MountDir
   173  			break
   174  		}
   175  	}
   176  
   177  	if mountPoint == "" {
   178  		return "", ErrMountNotFound
   179  	}
   180  
   181  	return mountPoint, nil
   182  }
   183  
   184  func isWritableMount(entry *osutil.MountInfoEntry) bool {
   185  	// example mountinfo entry:
   186  	// 26 27 8:3 / /writable rw,relatime shared:7 - ext4 /dev/sda3 rw,data=ordered
   187  	return entry.Root == "/" && entry.MountDir == "/writable" && entry.FsType == "ext4"
   188  }
   189  
   190  func findDeviceForWritable() (device string, err error) {
   191  	mountInfo, err := osutil.LoadMountInfo()
   192  	if err != nil {
   193  		return "", fmt.Errorf("cannot read mount info: %v", err)
   194  	}
   195  	for _, entry := range mountInfo {
   196  		if isWritableMount(entry) {
   197  			return entry.MountSource, nil
   198  		}
   199  	}
   200  	return "", ErrDeviceNotFound
   201  }
   202  
   203  func findParentDeviceWithWritableFallback() (string, error) {
   204  	partitionWritable, err := findDeviceForWritable()
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  	return ParentDiskFromMountSource(partitionWritable)
   209  }
   210  
   211  // ParentDiskFromMountSource will find the parent disk device for the given
   212  // partition. E.g. /dev/nvmen0n1p5 -> /dev/nvme0n1.
   213  //
   214  // When the mount source is a symlink, it is resolved to the actual device that
   215  // is mounted. Should the device be one created by device mapper, it is followed
   216  // up to the actual underlying block device. As an example, this is how devices
   217  // are followed with a /writable mounted from an encrypted volume:
   218  //
   219  // /dev/mapper/ubuntu-data-<uuid> (a symlink)
   220  //   ⤷ /dev/dm-0 (set up by device mapper)
   221  //       ⤷ /dev/hda4 (actual partition with the content)
   222  //          ⤷ /dev/hda (returned by this function)
   223  //
   224  func ParentDiskFromMountSource(mountSource string) (string, error) {
   225  	// mount source can be a symlink
   226  	st, err := os.Lstat(mountSource)
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  	if mode := st.Mode(); mode&os.ModeSymlink != 0 {
   231  		// resolve to actual device
   232  		target, err := filepath.EvalSymlinks(mountSource)
   233  		if err != nil {
   234  			return "", fmt.Errorf("cannot resolve mount source symlink %v: %v", mountSource, err)
   235  		}
   236  		mountSource = target
   237  	}
   238  	// /dev/sda3 -> sda3
   239  	devname := filepath.Base(mountSource)
   240  
   241  	if strings.HasPrefix(devname, "dm-") {
   242  		// looks like a device set up by device mapper
   243  		resolved, err := resolveParentOfDeviceMapperDevice(devname)
   244  		if err != nil {
   245  			return "", fmt.Errorf("cannot resolve device mapper device %v: %v", devname, err)
   246  		}
   247  		devname = resolved
   248  	}
   249  
   250  	// do not bother with investigating major/minor devices (inconsistent
   251  	// across block device types) or mangling strings, but look at sys
   252  	// hierarchy for block devices instead:
   253  	// /sys/block/sda               - main SCSI device
   254  	// /sys/block/sda/sda1          - partition 1
   255  	// /sys/block/sda/sda<n>        - partition n
   256  	// /sys/block/nvme0n1           - main NVME device
   257  	// /sys/block/nvme0n1/nvme0n1p1 - partition 1
   258  	matches, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/", devname))
   259  	if err != nil {
   260  		return "", fmt.Errorf("cannot glob /sys/block/ entries: %v", err)
   261  	}
   262  	if len(matches) != 1 {
   263  		return "", fmt.Errorf("unexpected number of matches (%v) for /sys/block/*/%s", len(matches), devname)
   264  	}
   265  
   266  	// at this point we have /sys/block/sda/sda3
   267  	// /sys/block/sda/sda3 -> /dev/sda
   268  	mainDev := filepath.Join(dirs.GlobalRootDir, "/dev/", filepath.Base(filepath.Dir(matches[0])))
   269  
   270  	if !osutil.FileExists(mainDev) {
   271  		return "", fmt.Errorf("device %v does not exist", mainDev)
   272  	}
   273  	return mainDev, nil
   274  }
   275  
   276  func resolveParentOfDeviceMapperDevice(devname string) (string, error) {
   277  	// devices set up by device mapper have /dev/block/dm-*/slaves directory
   278  	// which lists the devices that are upper in the chain, follow that to
   279  	// find the first device that is non-dm one
   280  	dmSlavesLevel := 0
   281  	const maxDmSlavesLevel = 5
   282  	for strings.HasPrefix(devname, "dm-") {
   283  		// /sys/block/dm-*/slaves/ lists a device that this dm device is part of
   284  		slavesGlob := filepath.Join(dirs.GlobalRootDir, "/sys/block", devname, "slaves/*")
   285  		slaves, err := filepath.Glob(slavesGlob)
   286  		if err != nil {
   287  			return "", fmt.Errorf("cannot glob slaves of dm device %v: %v", devname, err)
   288  		}
   289  		if len(slaves) != 1 {
   290  			return "", fmt.Errorf("unexpected number of dm device %v slaves: %v", devname, len(slaves))
   291  		}
   292  		devname = filepath.Base(slaves[0])
   293  
   294  		// if we're this deep in resolving dm devices, things are clearly getting out of hand
   295  		dmSlavesLevel++
   296  		if dmSlavesLevel >= maxDmSlavesLevel {
   297  			return "", fmt.Errorf("too many levels")
   298  		}
   299  
   300  	}
   301  	return devname, nil
   302  }