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 }