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 }