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 }