github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/layout.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 "fmt" 24 "os" 25 "path/filepath" 26 "sort" 27 "strings" 28 29 "github.com/snapcore/snapd/gadget/quantity" 30 "github.com/snapcore/snapd/kernel" 31 ) 32 33 // LayoutConstraints defines the constraints for arranging structures within a 34 // volume 35 type LayoutConstraints struct { 36 // NonMBRStartOffset is the default start offset of non-MBR structure in 37 // the volume. 38 NonMBRStartOffset quantity.Offset 39 // SkipResolveContent will skip resolving content paths 40 // and `$kernel:` style references 41 SkipResolveContent bool 42 } 43 44 // LaidOutVolume defines the size of a volume and arrangement of all the 45 // structures within it 46 type LaidOutVolume struct { 47 *Volume 48 // Size is the total size of the volume 49 Size quantity.Size 50 // LaidOutStructure is a list of structures within the volume, sorted 51 // by their start offsets 52 LaidOutStructure []LaidOutStructure 53 // RootDir is the root directory for volume data 54 RootDir string 55 } 56 57 // PartiallyLaidOutVolume defines the layout of volume structures, but lacks the 58 // details about the layout of raw image content within the bare structures. 59 type PartiallyLaidOutVolume struct { 60 *Volume 61 // LaidOutStructure is a list of structures within the volume, sorted 62 // by their start offsets 63 LaidOutStructure []LaidOutStructure 64 } 65 66 // LaidOutStructure describes a VolumeStructure that has been placed within the 67 // volume 68 type LaidOutStructure struct { 69 *VolumeStructure 70 // StartOffset defines the start offset of the structure within the 71 // enclosing volume 72 StartOffset quantity.Offset 73 // AbsoluteOffsetWrite is the resolved absolute position of offset-write 74 // for this structure element within the enclosing volume 75 AbsoluteOffsetWrite *quantity.Offset 76 // Index of the structure definition in gadget YAML 77 Index int 78 // LaidOutContent is a list of raw content inside the structure 79 LaidOutContent []LaidOutContent 80 // ResolvedContent is a list of filesystem content that has all 81 // relative paths or references resolved 82 ResolvedContent []ResolvedContent 83 } 84 85 // IsRoleMBR returns whether a structure's role is MBR or not. 86 // meh this function is weirdly placed, not sure what to do w/o making schemaMBR 87 // constant exported 88 func IsRoleMBR(ls LaidOutStructure) bool { 89 return ls.Role == schemaMBR 90 } 91 92 func (p LaidOutStructure) String() string { 93 return fmtIndexAndName(p.Index, p.Name) 94 } 95 96 type byStartOffset []LaidOutStructure 97 98 func (b byStartOffset) Len() int { return len(b) } 99 func (b byStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 100 func (b byStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset } 101 102 // LaidOutContent describes raw content that has been placed within the 103 // encompassing structure and volume 104 // 105 // TODO: this can't have "$kernel:" refs at this point, fail in validate 106 // for bare structures with "$kernel:" refs 107 type LaidOutContent struct { 108 *VolumeContent 109 110 // StartOffset defines the start offset of this content image 111 StartOffset quantity.Offset 112 // AbsoluteOffsetWrite is the resolved absolute position of offset-write 113 // for this content element within the enclosing volume 114 AbsoluteOffsetWrite *quantity.Offset 115 // Size is the maximum size occupied by this image 116 Size quantity.Size 117 // Index of the content in structure declaration inside gadget YAML 118 Index int 119 } 120 121 func (p LaidOutContent) String() string { 122 if p.Image != "" { 123 return fmt.Sprintf("#%v (%q@%#x{%v})", p.Index, p.Image, p.StartOffset, p.Size) 124 } 125 return fmt.Sprintf("#%v (source:%q)", p.Index, p.UnresolvedSource) 126 } 127 128 type ResolvedContent struct { 129 *VolumeContent 130 131 // ResolvedSource is the absolute path of the Source after resolving 132 // any references (e.g. to a "$kernel:" snap). 133 ResolvedSource string 134 135 // KernelUpdate is true if this content comes from the kernel 136 // and has the "Update" property set 137 KernelUpdate bool 138 } 139 140 func layoutVolumeStructures(volume *Volume, constraints LayoutConstraints) (structures []LaidOutStructure, byName map[string]*LaidOutStructure, err error) { 141 previousEnd := quantity.Offset(0) 142 structures = make([]LaidOutStructure, len(volume.Structure)) 143 byName = make(map[string]*LaidOutStructure, len(volume.Structure)) 144 145 for idx, s := range volume.Structure { 146 var start quantity.Offset 147 if s.Offset == nil { 148 if s.Role != schemaMBR && previousEnd < constraints.NonMBRStartOffset { 149 start = constraints.NonMBRStartOffset 150 } else { 151 start = previousEnd 152 } 153 } else { 154 start = *s.Offset 155 } 156 157 end := start + quantity.Offset(s.Size) 158 ps := LaidOutStructure{ 159 VolumeStructure: &volume.Structure[idx], 160 StartOffset: start, 161 Index: idx, 162 } 163 164 if ps.Name != "" { 165 byName[ps.Name] = &ps 166 } 167 168 structures[idx] = ps 169 170 previousEnd = end 171 } 172 173 // sort by starting offset 174 sort.Sort(byStartOffset(structures)) 175 176 previousEnd = quantity.Offset(0) 177 for idx, ps := range structures { 178 if ps.StartOffset < previousEnd { 179 return nil, nil, fmt.Errorf("cannot lay out volume, structure %v overlaps with preceding structure %v", ps, structures[idx-1]) 180 } 181 previousEnd = ps.StartOffset + quantity.Offset(ps.Size) 182 183 offsetWrite, err := resolveOffsetWrite(ps.OffsetWrite, byName) 184 if err != nil { 185 return nil, nil, fmt.Errorf("cannot resolve offset-write of structure %v: %v", ps, err) 186 } 187 structures[idx].AbsoluteOffsetWrite = offsetWrite 188 } 189 190 return structures, byName, nil 191 } 192 193 // LayoutVolumePartially attempts to lay out only the structures in the volume using provided constraints 194 func LayoutVolumePartially(volume *Volume, constraints LayoutConstraints) (*PartiallyLaidOutVolume, error) { 195 structures, _, err := layoutVolumeStructures(volume, constraints) 196 if err != nil { 197 return nil, err 198 } 199 200 vol := &PartiallyLaidOutVolume{ 201 Volume: volume, 202 LaidOutStructure: structures, 203 } 204 return vol, nil 205 } 206 207 // LayoutVolume attempts to completely lay out the volume, that is the 208 // structures and their content, using provided constraints 209 func LayoutVolume(gadgetRootDir, kernelRootDir string, volume *Volume, constraints LayoutConstraints) (*LaidOutVolume, error) { 210 var err error 211 212 var kernelInfo *kernel.Info 213 if !constraints.SkipResolveContent { 214 // TODO:UC20: check and error if kernelRootDir == "" here 215 // This needs the upper layer of gadget updates to be 216 // updated to pass the kernel root first. 217 // 218 // Note that the kernelRootDir may reference the running 219 // kernel if there is a gadget update or the new kernel if 220 // there is a kernel update. 221 kernelInfo, err = kernel.ReadInfo(kernelRootDir) 222 if err != nil { 223 return nil, err 224 } 225 } 226 227 structures, byName, err := layoutVolumeStructures(volume, constraints) 228 if err != nil { 229 return nil, err 230 } 231 232 farthestEnd := quantity.Offset(0) 233 fartherstOffsetWrite := quantity.Offset(0) 234 235 for idx, ps := range structures { 236 if ps.AbsoluteOffsetWrite != nil && *ps.AbsoluteOffsetWrite > fartherstOffsetWrite { 237 fartherstOffsetWrite = *ps.AbsoluteOffsetWrite 238 } 239 if end := ps.StartOffset + quantity.Offset(ps.Size); end > farthestEnd { 240 farthestEnd = end 241 } 242 243 // lay out raw content 244 content, err := layOutStructureContent(gadgetRootDir, &structures[idx], byName) 245 if err != nil { 246 return nil, err 247 } 248 249 for _, c := range content { 250 if c.AbsoluteOffsetWrite != nil && *c.AbsoluteOffsetWrite > fartherstOffsetWrite { 251 fartherstOffsetWrite = *c.AbsoluteOffsetWrite 252 } 253 } 254 255 structures[idx].LaidOutContent = content 256 257 // resolve filesystem content 258 if !constraints.SkipResolveContent { 259 resolvedContent, err := resolveVolumeContent(gadgetRootDir, kernelRootDir, kernelInfo, &structures[idx], nil) 260 if err != nil { 261 return nil, err 262 } 263 structures[idx].ResolvedContent = resolvedContent 264 } 265 } 266 267 volumeSize := quantity.Size(farthestEnd) 268 if fartherstOffsetWrite+quantity.Offset(SizeLBA48Pointer) > farthestEnd { 269 volumeSize = quantity.Size(fartherstOffsetWrite) + SizeLBA48Pointer 270 } 271 272 vol := &LaidOutVolume{ 273 Volume: volume, 274 Size: volumeSize, 275 LaidOutStructure: structures, 276 RootDir: gadgetRootDir, 277 } 278 return vol, nil 279 } 280 281 func resolveVolumeContent(gadgetRootDir, kernelRootDir string, kernelInfo *kernel.Info, ps *LaidOutStructure, filter ResolvedContentFilterFunc) ([]ResolvedContent, error) { 282 if !ps.HasFilesystem() { 283 // structures without a file system are not resolved here 284 return nil, nil 285 } 286 if len(ps.Content) == 0 { 287 return nil, nil 288 } 289 290 content := make([]ResolvedContent, 0, len(ps.Content)) 291 for idx := range ps.Content { 292 resolvedSource, kupdate, err := resolveContentPathOrRef(gadgetRootDir, kernelRootDir, kernelInfo, ps.Content[idx].UnresolvedSource) 293 if err != nil { 294 return nil, fmt.Errorf("cannot resolve content for structure %v at index %v: %v", ps, idx, err) 295 } 296 rc := ResolvedContent{ 297 VolumeContent: &ps.Content[idx], 298 ResolvedSource: resolvedSource, 299 KernelUpdate: kupdate, 300 } 301 if filter != nil && !filter(&rc) { 302 continue 303 } 304 content = append(content, rc) 305 } 306 307 return content, nil 308 } 309 310 // resolveContentPathOrRef resolves the relative path from gadget 311 // assets and any "$kernel:" references from "pathOrRef" using the 312 // provided gadget/kernel directories and the kernel info. It returns 313 // an absolute path, a flag indicating whether the content is part of 314 // a kernel update, or an error. 315 func resolveContentPathOrRef(gadgetRootDir, kernelRootDir string, kernelInfo *kernel.Info, pathOrRef string) (resolved string, kupdate bool, err error) { 316 317 // TODO: add kernelRootDir == "" error too once all the higher 318 // layers in devicestate call gadget.Update() with a 319 // kernel dir set 320 switch { 321 case gadgetRootDir == "": 322 return "", false, fmt.Errorf("internal error: gadget root dir cannot beempty") 323 case pathOrRef == "": 324 return "", false, fmt.Errorf("cannot use empty source") 325 } 326 327 // content may refer to "$kernel:<name>/<content>" 328 var resolvedSource string 329 if strings.HasPrefix(pathOrRef, "$kernel:") { 330 wantedAsset, wantedContent, err := splitKernelRef(pathOrRef) 331 if err != nil { 332 return "", false, fmt.Errorf("cannot parse kernel ref: %v", err) 333 } 334 kernelAsset, ok := kernelInfo.Assets[wantedAsset] 335 if !ok { 336 return "", false, fmt.Errorf("cannot find %q in kernel info from %q", wantedAsset, kernelRootDir) 337 } 338 // look for exact content match or for a directory prefix match 339 found := false 340 for _, kcontent := range kernelAsset.Content { 341 if wantedContent == kcontent { 342 found = true 343 break 344 } 345 // ensure we only check subdirs 346 suffix := "" 347 if !strings.HasSuffix(kcontent, "/") { 348 suffix = "/" 349 } 350 if strings.HasPrefix(wantedContent, kcontent+suffix) { 351 found = true 352 break 353 } 354 } 355 if !found { 356 return "", false, fmt.Errorf("cannot find wanted kernel content %q in %q", wantedContent, kernelRootDir) 357 } 358 resolvedSource = filepath.Join(kernelRootDir, wantedContent) 359 kupdate = kernelAsset.Update 360 } else { 361 resolvedSource = filepath.Join(gadgetRootDir, pathOrRef) 362 } 363 364 // restore trailing / if one was there 365 if strings.HasSuffix(pathOrRef, "/") { 366 resolvedSource += "/" 367 } 368 369 return resolvedSource, kupdate, nil 370 } 371 372 type byContentStartOffset []LaidOutContent 373 374 func (b byContentStartOffset) Len() int { return len(b) } 375 func (b byContentStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 376 func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset } 377 378 func getImageSize(path string) (quantity.Size, error) { 379 stat, err := os.Stat(path) 380 if err != nil { 381 return 0, err 382 } 383 return quantity.Size(stat.Size()), nil 384 } 385 386 func layOutStructureContent(gadgetRootDir string, ps *LaidOutStructure, known map[string]*LaidOutStructure) ([]LaidOutContent, error) { 387 if ps.HasFilesystem() { 388 // structures with a filesystem do not need any extra layout 389 return nil, nil 390 } 391 if len(ps.Content) == 0 { 392 return nil, nil 393 } 394 395 content := make([]LaidOutContent, len(ps.Content)) 396 previousEnd := quantity.Offset(0) 397 398 for idx, c := range ps.Content { 399 imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image)) 400 if err != nil { 401 return nil, fmt.Errorf("cannot lay out structure %v: content %q: %v", ps, c.Image, err) 402 } 403 404 var start quantity.Offset 405 if c.Offset != nil { 406 start = *c.Offset 407 } else { 408 start = previousEnd 409 } 410 411 actualSize := imageSize 412 413 if c.Size != 0 { 414 if c.Size < imageSize { 415 return nil, fmt.Errorf("cannot lay out structure %v: content %q size %v is larger than declared %v", ps, c.Image, actualSize, c.Size) 416 } 417 actualSize = c.Size 418 } 419 420 offsetWrite, err := resolveOffsetWrite(c.OffsetWrite, known) 421 if err != nil { 422 return nil, fmt.Errorf("cannot resolve offset-write of structure %v content %q: %v", ps, c.Image, err) 423 } 424 425 content[idx] = LaidOutContent{ 426 VolumeContent: &ps.Content[idx], 427 Size: actualSize, 428 StartOffset: ps.StartOffset + start, 429 Index: idx, 430 // break for gofmt < 1.11 431 AbsoluteOffsetWrite: offsetWrite, 432 } 433 previousEnd = start + quantity.Offset(actualSize) 434 if quantity.Size(previousEnd) > ps.Size { 435 return nil, fmt.Errorf("cannot lay out structure %v: content %q does not fit in the structure", ps, c.Image) 436 } 437 } 438 439 sort.Sort(byContentStartOffset(content)) 440 441 previousEnd = ps.StartOffset 442 for idx, pc := range content { 443 if pc.StartOffset < previousEnd { 444 return nil, fmt.Errorf("cannot lay out structure %v: content %q overlaps with preceding image %q", ps, pc.Image, content[idx-1].Image) 445 } 446 previousEnd = pc.StartOffset + quantity.Offset(pc.Size) 447 } 448 449 return content, nil 450 } 451 452 func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*quantity.Offset, error) { 453 if offsetWrite == nil { 454 return nil, nil 455 } 456 457 var relativeToOffset quantity.Offset 458 if offsetWrite.RelativeTo != "" { 459 otherStruct, ok := knownStructs[offsetWrite.RelativeTo] 460 if !ok { 461 return nil, fmt.Errorf("refers to an unknown structure %q", offsetWrite.RelativeTo) 462 } 463 relativeToOffset = otherStruct.StartOffset 464 } 465 466 resolvedOffsetWrite := relativeToOffset + offsetWrite.Offset 467 return &resolvedOffsetWrite, nil 468 } 469 470 // ShiftStructureTo translates the starting offset of a laid out structure and 471 // its content to the provided offset. 472 func ShiftStructureTo(ps LaidOutStructure, offset quantity.Offset) LaidOutStructure { 473 change := int64(offset - ps.StartOffset) 474 475 newPs := ps 476 newPs.StartOffset = quantity.Offset(int64(ps.StartOffset) + change) 477 478 newPs.LaidOutContent = make([]LaidOutContent, len(ps.LaidOutContent)) 479 for idx, pc := range ps.LaidOutContent { 480 newPc := pc 481 newPc.StartOffset = quantity.Offset(int64(pc.StartOffset) + change) 482 newPs.LaidOutContent[idx] = newPc 483 } 484 return newPs 485 } 486 487 func isLayoutCompatible(current, new *PartiallyLaidOutVolume) error { 488 if current.ID != new.ID { 489 return fmt.Errorf("incompatible ID change from %v to %v", current.ID, new.ID) 490 } 491 if current.Schema != new.Schema { 492 return fmt.Errorf("incompatible schema change from %v to %v", 493 current.Schema, new.Schema) 494 } 495 if current.Bootloader != new.Bootloader { 496 return fmt.Errorf("incompatible bootloader change from %v to %v", 497 current.Bootloader, new.Bootloader) 498 } 499 500 // XXX: the code below asssumes both volumes have the same number of 501 // structures, this limitation may be lifted later 502 if len(current.LaidOutStructure) != len(new.LaidOutStructure) { 503 return fmt.Errorf("incompatible change in the number of structures from %v to %v", 504 len(current.LaidOutStructure), len(new.LaidOutStructure)) 505 } 506 507 // at the structure level we expect the volume to be identical 508 for i := range current.LaidOutStructure { 509 from := ¤t.LaidOutStructure[i] 510 to := &new.LaidOutStructure[i] 511 if err := canUpdateStructure(from, to, new.Schema); err != nil { 512 return fmt.Errorf("incompatible structure %v change: %v", to, err) 513 } 514 } 515 return nil 516 }