github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/gadget.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-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 gadget 21 22 import ( 23 "bufio" 24 "bytes" 25 "errors" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "path/filepath" 30 "regexp" 31 "sort" 32 "strings" 33 34 "gopkg.in/yaml.v2" 35 36 "github.com/snapcore/snapd/asserts" 37 "github.com/snapcore/snapd/gadget/edition" 38 "github.com/snapcore/snapd/gadget/quantity" 39 "github.com/snapcore/snapd/metautil" 40 "github.com/snapcore/snapd/osutil" 41 "github.com/snapcore/snapd/snap" 42 "github.com/snapcore/snapd/snap/naming" 43 "github.com/snapcore/snapd/snap/snapfile" 44 "github.com/snapcore/snapd/strutil" 45 ) 46 47 const ( 48 // schemaMBR identifies a Master Boot Record partitioning schema, or an 49 // MBR like role 50 schemaMBR = "mbr" 51 // schemaGPT identifies a GUID Partition Table partitioning schema 52 schemaGPT = "gpt" 53 54 SystemBoot = "system-boot" 55 SystemData = "system-data" 56 SystemSeed = "system-seed" 57 SystemSave = "system-save" 58 59 // extracted kernels for all uc systems 60 bootImage = "system-boot-image" 61 62 // extracted kernels for recovery kernels for uc20 specifically 63 seedBootImage = "system-seed-image" 64 65 // bootloader specific partition which stores bootloader environment vars 66 // for purposes of booting normal run mode on uc20 and all modes on 67 // uc16 and uc18 68 bootSelect = "system-boot-select" 69 70 // bootloader specific partition which stores bootloader environment vars 71 // for purposes of booting recovery systems on uc20, i.e. recover or install 72 seedBootSelect = "system-seed-select" 73 74 // implicitSystemDataLabel is the implicit filesystem label of structure 75 // of system-data role 76 implicitSystemDataLabel = "writable" 77 78 // UC20 filesystem labels for roles 79 ubuntuBootLabel = "ubuntu-boot" 80 ubuntuSeedLabel = "ubuntu-seed" 81 ubuntuDataLabel = "ubuntu-data" 82 ubuntuSaveLabel = "ubuntu-save" 83 84 // only supported for legacy reasons 85 legacyBootImage = "bootimg" 86 legacyBootSelect = "bootselect" 87 ) 88 89 var ( 90 validVolumeName = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]+$") 91 validTypeID = regexp.MustCompile("^[0-9A-F]{2}$") 92 validGUUID = regexp.MustCompile("^(?i)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$") 93 ) 94 95 type Info struct { 96 Volumes map[string]*Volume `yaml:"volumes,omitempty"` 97 98 // Default configuration for snaps (snap-id => key => value). 99 Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"` 100 101 Connections []Connection `yaml:"connections"` 102 } 103 104 // Volume defines the structure and content for the image to be written into a 105 // block device. 106 type Volume struct { 107 // Schema describes the schema used for the volume 108 Schema string `yaml:"schema"` 109 // Bootloader names the bootloader used by the volume 110 Bootloader string `yaml:"bootloader"` 111 // ID is a 2-hex digit disk ID or GPT GUID 112 ID string `yaml:"id"` 113 // Structure describes the structures that are part of the volume 114 Structure []VolumeStructure `yaml:"structure"` 115 } 116 117 // VolumeStructure describes a single structure inside a volume. A structure can 118 // represent a partition, Master Boot Record, or any other contiguous range 119 // within the volume. 120 type VolumeStructure struct { 121 // Name, when non empty, provides the name of the structure 122 Name string `yaml:"name"` 123 // Label provides the filesystem label 124 Label string `yaml:"filesystem-label"` 125 // Offset defines a starting offset of the structure 126 Offset *quantity.Offset `yaml:"offset"` 127 // OffsetWrite describes a 32-bit address, within the volume, at which 128 // the offset of current structure will be written. The position may be 129 // specified as a byte offset relative to the start of a named structure 130 OffsetWrite *RelativeOffset `yaml:"offset-write"` 131 // Size of the structure 132 Size quantity.Size `yaml:"size"` 133 // Type of the structure, which can be 2-hex digit MBR partition, 134 // 36-char GUID partition, comma separated <mbr>,<guid> for hybrid 135 // partitioning schemes, or 'bare' when the structure is not considered 136 // a partition. 137 // 138 // For backwards compatibility type 'mbr' is also accepted, and the 139 // structure is treated as if it is of role 'mbr'. 140 Type string `yaml:"type"` 141 // Role describes the role of given structure, can be one of 142 // 'mbr', 'system-data', 'system-boot', 'system-boot-image', 143 // 'system-boot-select' or 'system-recovery-select'. Structures of type 'mbr', must have a 144 // size of 446 bytes and must start at 0 offset. 145 Role string `yaml:"role"` 146 // ID is the GPT partition ID 147 ID string `yaml:"id"` 148 // Filesystem used for the partition, 'vfat', 'ext4' or 'none' for 149 // structures of type 'bare' 150 Filesystem string `yaml:"filesystem"` 151 // Content of the structure 152 Content []VolumeContent `yaml:"content"` 153 Update VolumeUpdate `yaml:"update"` 154 } 155 156 // HasFilesystem returns true if the structure is using a filesystem. 157 func (vs *VolumeStructure) HasFilesystem() bool { 158 return vs.Filesystem != "none" && vs.Filesystem != "" 159 } 160 161 // IsPartition returns true when the structure describes a partition in a block 162 // device. 163 func (vs *VolumeStructure) IsPartition() bool { 164 return vs.Type != "bare" && vs.Role != schemaMBR 165 } 166 167 // VolumeContent defines the contents of the structure. The content can be 168 // either files within a filesystem described by the structure or raw images 169 // written into the area of a bare structure. 170 type VolumeContent struct { 171 // UnresovedSource is the data of the partition relative to 172 // the gadget base directory 173 UnresolvedSource string `yaml:"source"` 174 // Target is the location of the data inside the root filesystem 175 Target string `yaml:"target"` 176 177 // Image names the image, relative to gadget base directory, to be used 178 // for a 'bare' type structure 179 Image string `yaml:"image"` 180 // Offset the image is written at 181 Offset *quantity.Offset `yaml:"offset"` 182 // OffsetWrite describes a 32-bit address, within the volume, at which 183 // the offset of current image will be written. The position may be 184 // specified as a byte offset relative to the start of a named structure 185 OffsetWrite *RelativeOffset `yaml:"offset-write"` 186 // Size of the image, when empty size is calculated by looking at the 187 // image 188 Size quantity.Size `yaml:"size"` 189 190 Unpack bool `yaml:"unpack"` 191 } 192 193 func (vc VolumeContent) String() string { 194 if vc.Image != "" { 195 return fmt.Sprintf("image:%s", vc.Image) 196 } 197 return fmt.Sprintf("source:%s", vc.UnresolvedSource) 198 } 199 200 type VolumeUpdate struct { 201 Edition edition.Number `yaml:"edition"` 202 Preserve []string `yaml:"preserve"` 203 } 204 205 // GadgetConnect describes an interface connection requested by the gadget 206 // between seeded snaps. The syntax is of a mapping like: 207 // 208 // plug: (<plug-snap-id>|system):plug 209 // [slot: (<slot-snap-id>|system):slot] 210 // 211 // "system" indicates a system plug or slot. 212 // Fully omitting the slot part indicates a system slot with the same name 213 // as the plug. 214 type Connection struct { 215 Plug ConnectionPlug `yaml:"plug"` 216 Slot ConnectionSlot `yaml:"slot"` 217 } 218 219 type ConnectionPlug struct { 220 SnapID string 221 Plug string 222 } 223 224 func (gcplug *ConnectionPlug) Empty() bool { 225 return gcplug.SnapID == "" && gcplug.Plug == "" 226 } 227 228 func (gcplug *ConnectionPlug) UnmarshalYAML(unmarshal func(interface{}) error) error { 229 var s string 230 if err := unmarshal(&s); err != nil { 231 return err 232 } 233 snapID, name, err := parseSnapIDColonName(s) 234 if err != nil { 235 return fmt.Errorf("in gadget connection plug: %v", err) 236 } 237 gcplug.SnapID = snapID 238 gcplug.Plug = name 239 return nil 240 } 241 242 type ConnectionSlot struct { 243 SnapID string 244 Slot string 245 } 246 247 func (gcslot *ConnectionSlot) Empty() bool { 248 return gcslot.SnapID == "" && gcslot.Slot == "" 249 } 250 251 func (gcslot *ConnectionSlot) UnmarshalYAML(unmarshal func(interface{}) error) error { 252 var s string 253 if err := unmarshal(&s); err != nil { 254 return err 255 } 256 snapID, name, err := parseSnapIDColonName(s) 257 if err != nil { 258 return fmt.Errorf("in gadget connection slot: %v", err) 259 } 260 gcslot.SnapID = snapID 261 gcslot.Slot = name 262 return nil 263 } 264 265 func parseSnapIDColonName(s string) (snapID, name string, err error) { 266 parts := strings.Split(s, ":") 267 if len(parts) == 2 { 268 snapID = parts[0] 269 name = parts[1] 270 } 271 if snapID == "" || name == "" { 272 return "", "", fmt.Errorf(`expected "(<snap-id>|system):name" not %q`, s) 273 } 274 return snapID, name, nil 275 } 276 277 func systemOrSnapID(s string) bool { 278 if s != "system" && naming.ValidateSnapID(s) != nil { 279 return false 280 } 281 return true 282 } 283 284 // Model carries characteristics about the model that are relevant to gadget. 285 // Note *asserts.Model implements this, and that's the expected use case. 286 type Model interface { 287 Classic() bool 288 Grade() asserts.ModelGrade 289 } 290 291 func classicOrUndetermined(m Model) bool { 292 return m == nil || m.Classic() 293 } 294 295 func wantsSystemSeed(m Model) bool { 296 return m != nil && m.Grade() != asserts.ModelGradeUnset 297 } 298 299 // InfoFromGadgetYaml parses the provided gadget metadata. 300 // If model is nil only self-consistency checks are performed. 301 // If model is not nil implied values for filesystem labels will be set 302 // as well, based on whether the model is for classic, UC16/18 or UC20. 303 // UC gadget metadata is expected to have volumes definitions. 304 func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) { 305 var gi Info 306 307 if err := yaml.Unmarshal(gadgetYaml, &gi); err != nil { 308 return nil, fmt.Errorf("cannot parse gadget metadata: %v", err) 309 } 310 311 for k, v := range gi.Defaults { 312 if !systemOrSnapID(k) { 313 return nil, fmt.Errorf(`default stanza not keyed by "system" or snap-id: %s`, k) 314 } 315 dflt, err := metautil.NormalizeValue(v) 316 if err != nil { 317 return nil, fmt.Errorf("default value %q of %q: %v", v, k, err) 318 } 319 gi.Defaults[k] = dflt.(map[string]interface{}) 320 } 321 322 for i, gconn := range gi.Connections { 323 if gconn.Plug.Empty() { 324 return nil, errors.New("gadget connection plug cannot be empty") 325 } 326 if gconn.Slot.Empty() { 327 gi.Connections[i].Slot.SnapID = "system" 328 gi.Connections[i].Slot.Slot = gconn.Plug.Plug 329 } 330 } 331 332 if len(gi.Volumes) == 0 && classicOrUndetermined(model) { 333 // volumes can be left out on classic 334 // can still specify defaults though 335 return &gi, nil 336 } 337 338 // basic validation 339 var bootloadersFound int 340 knownFsLabelsPerVolume := make(map[string]map[string]bool, len(gi.Volumes)) 341 for name, v := range gi.Volumes { 342 if err := validateVolume(name, v, model, knownFsLabelsPerVolume); err != nil { 343 return nil, fmt.Errorf("invalid volume %q: %v", name, err) 344 } 345 346 switch v.Bootloader { 347 case "": 348 // pass 349 case "grub", "u-boot", "android-boot", "lk": 350 bootloadersFound += 1 351 default: 352 return nil, errors.New("bootloader must be one of grub, u-boot, android-boot or lk") 353 } 354 } 355 switch { 356 case bootloadersFound == 0: 357 return nil, errors.New("bootloader not declared in any volume") 358 case bootloadersFound > 1: 359 return nil, fmt.Errorf("too many (%d) bootloaders declared", bootloadersFound) 360 } 361 362 for name, v := range gi.Volumes { 363 if err := setImplicitForVolume(name, v, model, knownFsLabelsPerVolume[name]); err != nil { 364 return nil, fmt.Errorf("invalid volume %q: %v", name, err) 365 } 366 } 367 368 return &gi, nil 369 } 370 371 type volRuleset int 372 373 const ( 374 volRulesetUnknown volRuleset = iota 375 volRuleset16 376 volRuleset20 377 ) 378 379 func whichVolRuleset(model Model) volRuleset { 380 if model == nil { 381 return volRulesetUnknown 382 } 383 if model.Grade() != asserts.ModelGradeUnset { 384 return volRuleset20 385 } 386 return volRuleset16 387 } 388 389 func setImplicitForVolume(name string, vol *Volume, model Model, knownFsLabels map[string]bool) error { 390 rs := whichVolRuleset(model) 391 if vol.Schema == "" { 392 // default for schema is gpt 393 vol.Schema = schemaGPT 394 } 395 for i := range vol.Structure { 396 if err := setImplicitForVolumeStructure(&vol.Structure[i], rs, knownFsLabels); err != nil { 397 return err 398 } 399 } 400 return nil 401 } 402 403 func setImplicitForVolumeStructure(vs *VolumeStructure, rs volRuleset, knownFsLabels map[string]bool) error { 404 if vs.Role == "" && vs.Type == schemaMBR { 405 vs.Role = schemaMBR 406 return nil 407 } 408 if rs == volRuleset16 && vs.Role == "" && vs.Label == SystemBoot { 409 // legacy behavior, for gadgets that only specify a filesystem-label, eg. pc 410 vs.Role = SystemBoot 411 return nil 412 } 413 if vs.Label == "" { 414 var implicitLabel string 415 switch { 416 case rs == volRuleset16 && vs.Role == SystemData: 417 implicitLabel = implicitSystemDataLabel 418 case rs == volRuleset20 && vs.Role == SystemData: 419 implicitLabel = ubuntuDataLabel 420 case rs == volRuleset20 && vs.Role == SystemSeed: 421 implicitLabel = ubuntuSeedLabel 422 case rs == volRuleset20 && vs.Role == SystemBoot: 423 implicitLabel = ubuntuBootLabel 424 case rs == volRuleset20 && vs.Role == SystemSave: 425 implicitLabel = ubuntuSaveLabel 426 } 427 if implicitLabel != "" { 428 if knownFsLabels[implicitLabel] { 429 return fmt.Errorf("filesystem label %q is implied by %s role but was already set elsewhere", implicitLabel, vs.Role) 430 } 431 knownFsLabels[implicitLabel] = true 432 vs.Label = implicitLabel 433 } 434 } 435 return nil 436 } 437 438 func readInfo(f func(string) ([]byte, error), gadgetYamlFn string, model Model) (*Info, error) { 439 gmeta, err := f(gadgetYamlFn) 440 if classicOrUndetermined(model) && os.IsNotExist(err) { 441 // gadget.yaml is optional for classic gadgets 442 return &Info{}, nil 443 } 444 if err != nil { 445 return nil, err 446 } 447 448 return InfoFromGadgetYaml(gmeta, model) 449 } 450 451 // ReadInfo reads the gadget specific metadata from meta/gadget.yaml in the snap 452 // root directory. 453 // See ReadInfoAndValidate for a variant that does role-usage consistency 454 // validation like Validate. 455 func ReadInfo(gadgetSnapRootDir string, model Model) (*Info, error) { 456 gadgetYamlFn := filepath.Join(gadgetSnapRootDir, "meta", "gadget.yaml") 457 ginfo, err := readInfo(ioutil.ReadFile, gadgetYamlFn, model) 458 if err != nil { 459 return nil, err 460 } 461 return ginfo, nil 462 } 463 464 // ReadInfoAndValidate reads the gadget specific metadata from 465 // meta/gadget.yaml in the snap root directory. 466 // It also performs role-usage consistency validation as Validate does 467 // using the given constraints. See ReadInfo for a variant that does not. 468 // See also ValidateContent for further validating the content itself 469 // instead of the metadata. 470 func ReadInfoAndValidate(gadgetSnapRootDir string, model Model, validationConstraints *ValidationConstraints) (*Info, error) { 471 ginfo, err := ReadInfo(gadgetSnapRootDir, model) 472 if err != nil { 473 return nil, err 474 } 475 if err := Validate(ginfo, model, validationConstraints); err != nil { 476 return nil, err 477 } 478 return ginfo, err 479 } 480 481 // ReadInfoFromSnapFile reads the gadget specific metadata from 482 // meta/gadget.yaml in the given snap container. 483 // It also performs role-usage consistency validation as Validate does. 484 // See ReadInfoFromSnapFileNoValidate for a variant that does not. 485 func ReadInfoFromSnapFile(snapf snap.Container, model Model) (*Info, error) { 486 ginfo, err := ReadInfoFromSnapFileNoValidate(snapf, model) 487 if err != nil { 488 return nil, err 489 } 490 if err := Validate(ginfo, model, nil); err != nil { 491 return nil, err 492 } 493 return ginfo, nil 494 } 495 496 // ReadInfoFromSnapFileNoValidate reads the gadget specific metadata from 497 // meta/gadget.yaml in the given snap container. 498 // See ReadInfoFromSnapFile for a variant that does role-usage consistency 499 // validation like Validate as well. 500 func ReadInfoFromSnapFileNoValidate(snapf snap.Container, model Model) (*Info, error) { 501 gadgetYamlFn := "meta/gadget.yaml" 502 ginfo, err := readInfo(snapf.ReadFile, gadgetYamlFn, model) 503 if err != nil { 504 return nil, err 505 } 506 return ginfo, nil 507 } 508 509 func fmtIndexAndName(idx int, name string) string { 510 if name != "" { 511 return fmt.Sprintf("#%v (%q)", idx, name) 512 } 513 return fmt.Sprintf("#%v", idx) 514 } 515 516 func validateVolume(name string, vol *Volume, model Model, knownFsLabelsPerVolume map[string]map[string]bool) error { 517 if !validVolumeName.MatchString(name) { 518 return errors.New("invalid name") 519 } 520 if vol.Schema != "" && vol.Schema != schemaGPT && vol.Schema != schemaMBR { 521 return fmt.Errorf("invalid schema %q", vol.Schema) 522 } 523 524 // named structures, for cross-referencing relative offset-write names 525 knownStructures := make(map[string]*LaidOutStructure, len(vol.Structure)) 526 // for uniqueness of filesystem labels 527 knownFsLabels := make(map[string]bool, len(vol.Structure)) 528 // for validating structure overlap 529 structures := make([]LaidOutStructure, len(vol.Structure)) 530 531 if knownFsLabelsPerVolume != nil { 532 knownFsLabelsPerVolume[name] = knownFsLabels 533 } 534 535 previousEnd := quantity.Offset(0) 536 // TODO: should we also validate that if there is a system-recovery-select 537 // role there should also be at least 2 system-recovery-image roles and 538 // same for system-boot-select and at least 2 system-boot-image roles? 539 for idx, s := range vol.Structure { 540 if err := validateVolumeStructure(&s, vol); err != nil { 541 return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err) 542 } 543 var start quantity.Offset 544 if s.Offset != nil { 545 start = *s.Offset 546 } else { 547 start = previousEnd 548 } 549 end := start + quantity.Offset(s.Size) 550 ps := LaidOutStructure{ 551 VolumeStructure: &vol.Structure[idx], 552 StartOffset: start, 553 Index: idx, 554 } 555 structures[idx] = ps 556 if s.Name != "" { 557 if _, ok := knownStructures[s.Name]; ok { 558 return fmt.Errorf("structure name %q is not unique", s.Name) 559 } 560 // keep track of named structures 561 knownStructures[s.Name] = &ps 562 } 563 if s.Label != "" { 564 if seen := knownFsLabels[s.Label]; seen { 565 return fmt.Errorf("filesystem label %q is not unique", s.Label) 566 } 567 knownFsLabels[s.Label] = true 568 } 569 570 previousEnd = end 571 } 572 573 // sort by starting offset 574 sort.Sort(byStartOffset(structures)) 575 576 return validateCrossVolumeStructure(structures, knownStructures) 577 } 578 579 // isMBR returns whether the structure is the MBR and can be used before setImplicitForVolume 580 func isMBR(vs *VolumeStructure) bool { 581 if vs.Role == schemaMBR { 582 return true 583 } 584 if vs.Role == "" && vs.Type == schemaMBR { 585 return true 586 } 587 return false 588 } 589 590 func validateCrossVolumeStructure(structures []LaidOutStructure, knownStructures map[string]*LaidOutStructure) error { 591 previousEnd := quantity.Offset(0) 592 // cross structure validation: 593 // - relative offsets that reference other structures by name 594 // - laid out structure overlap 595 // use structures laid out within the volume 596 for pidx, ps := range structures { 597 if isMBR(ps.VolumeStructure) { 598 if ps.StartOffset != 0 { 599 return fmt.Errorf(`structure %v has "mbr" role and must start at offset 0`, ps) 600 } 601 } 602 if ps.OffsetWrite != nil && ps.OffsetWrite.RelativeTo != "" { 603 // offset-write using a named structure 604 other := knownStructures[ps.OffsetWrite.RelativeTo] 605 if other == nil { 606 return fmt.Errorf("structure %v refers to an unknown structure %q", 607 ps, ps.OffsetWrite.RelativeTo) 608 } 609 } 610 611 if ps.StartOffset < previousEnd { 612 previous := structures[pidx-1] 613 return fmt.Errorf("structure %v overlaps with the preceding structure %v", ps, previous) 614 } 615 previousEnd = ps.StartOffset + quantity.Offset(ps.Size) 616 617 if ps.HasFilesystem() { 618 // content relative offset only possible if it's a bare structure 619 continue 620 } 621 for cidx, c := range ps.Content { 622 if c.OffsetWrite == nil || c.OffsetWrite.RelativeTo == "" { 623 continue 624 } 625 relativeToStructure := knownStructures[c.OffsetWrite.RelativeTo] 626 if relativeToStructure == nil { 627 return fmt.Errorf("structure %v, content %v refers to an unknown structure %q", 628 ps, fmtIndexAndName(cidx, c.Image), c.OffsetWrite.RelativeTo) 629 } 630 } 631 } 632 return nil 633 } 634 635 func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { 636 if vs.Size == 0 { 637 return errors.New("missing size") 638 } 639 if err := validateStructureType(vs.Type, vol); err != nil { 640 return fmt.Errorf("invalid type %q: %v", vs.Type, err) 641 } 642 if err := validateRole(vs, vol); err != nil { 643 var what string 644 if vs.Role != "" { 645 what = fmt.Sprintf("role %q", vs.Role) 646 } else { 647 what = fmt.Sprintf("implicit role %q", vs.Type) 648 } 649 return fmt.Errorf("invalid %s: %v", what, err) 650 } 651 if vs.Filesystem != "" && !strutil.ListContains([]string{"ext4", "vfat", "none"}, vs.Filesystem) { 652 return fmt.Errorf("invalid filesystem %q", vs.Filesystem) 653 } 654 655 var contentChecker func(*VolumeContent) error 656 657 if !vs.HasFilesystem() { 658 contentChecker = validateBareContent 659 } else { 660 contentChecker = validateFilesystemContent 661 } 662 for i, c := range vs.Content { 663 if err := contentChecker(&c); err != nil { 664 return fmt.Errorf("invalid content #%v: %v", i, err) 665 } 666 } 667 668 if err := validateStructureUpdate(&vs.Update, vs); err != nil { 669 return err 670 } 671 672 // TODO: validate structure size against sector-size; ubuntu-image uses 673 // a tmp file to find out the default sector size of the device the tmp 674 // file is created on 675 return nil 676 } 677 678 func validateStructureType(s string, vol *Volume) error { 679 // Type can be one of: 680 // - "mbr" (backwards compatible) 681 // - "bare" 682 // - [0-9A-Z]{2} - MBR type 683 // - GPT UUID 684 // - hybrid ID 685 // 686 // Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID 687 // example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 688 689 schema := vol.Schema 690 if schema == "" { 691 schema = schemaGPT 692 } 693 694 if s == "" { 695 return errors.New(`type is not specified`) 696 } 697 698 if s == "bare" { 699 // unknonwn blob 700 return nil 701 } 702 703 if s == schemaMBR { 704 // backward compatibility for type: mbr 705 return nil 706 } 707 708 var isGPT, isMBR bool 709 710 idx := strings.IndexRune(s, ',') 711 if idx == -1 { 712 // just ID 713 switch { 714 case validTypeID.MatchString(s): 715 isMBR = true 716 case validGUUID.MatchString(s): 717 isGPT = true 718 default: 719 return fmt.Errorf("invalid format") 720 } 721 } else { 722 // hybrid ID 723 code := s[:idx] 724 guid := s[idx+1:] 725 if len(code) != 2 || len(guid) != 36 || !validTypeID.MatchString(code) || !validGUUID.MatchString(guid) { 726 return fmt.Errorf("invalid format of hybrid type") 727 } 728 } 729 730 if schema != schemaGPT && isGPT { 731 // type: <uuid> is only valid for GPT volumes 732 return fmt.Errorf("GUID structure type with non-GPT schema %q", vol.Schema) 733 } 734 if schema != schemaMBR && isMBR { 735 return fmt.Errorf("MBR structure type with non-MBR schema %q", vol.Schema) 736 } 737 738 return nil 739 } 740 741 func validateRole(vs *VolumeStructure, vol *Volume) error { 742 if vs.Type == "bare" { 743 if vs.Role != "" && vs.Role != schemaMBR { 744 return fmt.Errorf("conflicting type: %q", vs.Type) 745 } 746 } 747 vsRole := vs.Role 748 if vs.Type == schemaMBR { 749 if vsRole != "" && vsRole != schemaMBR { 750 return fmt.Errorf(`conflicting legacy type: "mbr"`) 751 } 752 // backward compatibility 753 vsRole = schemaMBR 754 } 755 756 switch vsRole { 757 case SystemData, SystemSeed, SystemSave: 758 // roles have cross dependencies, consistency checks are done at 759 // the volume level 760 case schemaMBR: 761 if vs.Size > SizeMBR { 762 return errors.New("mbr structures cannot be larger than 446 bytes") 763 } 764 if vs.Offset != nil && *vs.Offset != 0 { 765 return errors.New("mbr structure must start at offset 0") 766 } 767 if vs.ID != "" { 768 return errors.New("mbr structure must not specify partition ID") 769 } 770 if vs.Filesystem != "" && vs.Filesystem != "none" { 771 return errors.New("mbr structures must not specify a file system") 772 } 773 case SystemBoot, bootImage, bootSelect, seedBootSelect, seedBootImage, "": 774 // noop 775 case legacyBootImage, legacyBootSelect: 776 // noop 777 // legacy role names were added in 2.42 can be removed 778 // on snapd epoch bump 779 default: 780 return fmt.Errorf("unsupported role") 781 } 782 return nil 783 } 784 785 func validateBareContent(vc *VolumeContent) error { 786 if vc.UnresolvedSource != "" || vc.Target != "" { 787 return fmt.Errorf("cannot use non-image content for bare file system") 788 } 789 if vc.Image == "" { 790 return fmt.Errorf("missing image file name") 791 } 792 return nil 793 } 794 795 func validateFilesystemContent(vc *VolumeContent) error { 796 if vc.Image != "" || vc.Offset != nil || vc.OffsetWrite != nil || vc.Size != 0 { 797 return fmt.Errorf("cannot use image content for non-bare file system") 798 } 799 if vc.UnresolvedSource == "" { 800 return fmt.Errorf("missing source") 801 } 802 if vc.Target == "" { 803 return fmt.Errorf("missing target") 804 } 805 return nil 806 } 807 808 func validateStructureUpdate(up *VolumeUpdate, vs *VolumeStructure) error { 809 if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 { 810 return errors.New("preserving files during update is not supported for non-filesystem structures") 811 } 812 813 names := make(map[string]bool, len(vs.Update.Preserve)) 814 for _, n := range vs.Update.Preserve { 815 if names[n] { 816 return fmt.Errorf(`duplicate "preserve" entry %q`, n) 817 } 818 names[n] = true 819 } 820 return nil 821 } 822 823 const ( 824 // SizeMBR is the maximum byte size of a structure of role 'mbr' 825 SizeMBR = quantity.Size(446) 826 // SizeLBA48Pointer is the byte size of a pointer value written at the 827 // location described by 'offset-write' 828 SizeLBA48Pointer = quantity.Size(4) 829 ) 830 831 // RelativeOffset describes an offset where structure data is written at. 832 // The position can be specified as byte-offset relative to the start of another 833 // named structure. 834 type RelativeOffset struct { 835 // RelativeTo names the structure relative to which the location of the 836 // address write will be calculated. 837 RelativeTo string 838 // Offset is a 32-bit value 839 Offset quantity.Offset 840 } 841 842 func (r *RelativeOffset) String() string { 843 if r == nil { 844 return "unspecified" 845 } 846 if r.RelativeTo != "" { 847 return fmt.Sprintf("%s+%d", r.RelativeTo, r.Offset) 848 } 849 return fmt.Sprintf("%d", r.Offset) 850 } 851 852 // parseRelativeOffset parses a string describing an offset that can be 853 // expressed relative to a named structure, with the format: [<name>+]<offset>. 854 func parseRelativeOffset(grs string) (*RelativeOffset, error) { 855 toWhat := "" 856 offsSpec := grs 857 if idx := strings.IndexRune(grs, '+'); idx != -1 { 858 toWhat, offsSpec = grs[:idx], grs[idx+1:] 859 if toWhat == "" { 860 return nil, errors.New("missing volume name") 861 } 862 } 863 if offsSpec == "" { 864 return nil, errors.New("missing offset") 865 } 866 867 offset, err := quantity.ParseOffset(offsSpec) 868 if err != nil { 869 return nil, fmt.Errorf("cannot parse offset %q: %v", offsSpec, err) 870 } 871 if offset > 4*1024*quantity.OffsetMiB { 872 return nil, fmt.Errorf("offset above 4G limit") 873 } 874 875 return &RelativeOffset{ 876 RelativeTo: toWhat, 877 Offset: offset, 878 }, nil 879 } 880 881 func (s *RelativeOffset) UnmarshalYAML(unmarshal func(interface{}) error) error { 882 var grs string 883 if err := unmarshal(&grs); err != nil { 884 return errors.New(`cannot unmarshal gadget relative offset`) 885 } 886 887 ro, err := parseRelativeOffset(grs) 888 if err != nil { 889 return fmt.Errorf("cannot parse relative offset %q: %v", grs, err) 890 } 891 *s = *ro 892 return nil 893 } 894 895 // IsCompatible checks whether the current and an update are compatible. Returns 896 // nil or an error describing the incompatibility. 897 func IsCompatible(current, new *Info) error { 898 // XXX: the only compatibility we have now is making sure that the new 899 // layout can be used on an existing volume 900 if len(new.Volumes) > 1 { 901 return fmt.Errorf("gadgets with multiple volumes are unsupported") 902 } 903 904 // XXX: the code below errors out with more than 1 volume in the current 905 // gadget, we allow this scenario in update but better bail out here and 906 // have users fix their gadgets 907 currentVol, newVol, err := resolveVolume(current, new) 908 if err != nil { 909 return err 910 } 911 912 if currentVol.Schema == "" || newVol.Schema == "" { 913 return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", currentVol.Schema, newVol.Schema) 914 } 915 916 // layout both volumes partially, without going deep into the layout of 917 // structure content, we only want to make sure that structures are 918 // comapatible 919 pCurrent, err := LayoutVolumePartially(currentVol, DefaultConstraints) 920 if err != nil { 921 return fmt.Errorf("cannot lay out the current volume: %v", err) 922 } 923 pNew, err := LayoutVolumePartially(newVol, DefaultConstraints) 924 if err != nil { 925 return fmt.Errorf("cannot lay out the new volume: %v", err) 926 } 927 if err := isLayoutCompatible(pCurrent, pNew); err != nil { 928 return fmt.Errorf("incompatible layout change: %v", err) 929 } 930 return nil 931 } 932 933 // LaidOutSystemVolumeFromGadget takes a gadget rootdir and lays out the 934 // partitions as specified. It returns one specific volume, which is the volume 935 // on which system-* roles/partitions exist, all other volumes are assumed to 936 // already be flashed and managed separately at image build/flash time, while 937 // the system-* roles can be manipulated on the returned volume during install 938 // mode. 939 func LaidOutSystemVolumeFromGadget(gadgetRoot, kernelRoot string, model Model) (*LaidOutVolume, error) { 940 // model should never be nil here 941 if model == nil { 942 return nil, fmt.Errorf("internal error: must have model to lay out system volumes from a gadget") 943 } 944 // rely on the basic validation from ReadInfo to ensure that the system-* 945 // roles are all on the same volume for example 946 info, err := ReadInfoAndValidate(gadgetRoot, model, nil) 947 if err != nil { 948 return nil, err 949 } 950 951 constraints := LayoutConstraints{ 952 NonMBRStartOffset: 1 * quantity.OffsetMiB, 953 } 954 955 // find the volume with the system-boot role on it, we already validated 956 // that the system-* roles are all on the same volume 957 for _, vol := range info.Volumes { 958 for _, structure := range vol.Structure { 959 // use the system-boot role to identify the system volume 960 if structure.Role == SystemBoot { 961 pvol, err := LayoutVolume(gadgetRoot, kernelRoot, vol, constraints) 962 if err != nil { 963 return nil, err 964 } 965 966 return pvol, nil 967 } 968 } 969 } 970 971 // this should be impossible, the validation above should ensure this 972 return nil, fmt.Errorf("internal error: gadget passed validation but does not have system-* roles on any volume") 973 } 974 975 func flatten(path string, cfg interface{}, out map[string]interface{}) { 976 if cfgMap, ok := cfg.(map[string]interface{}); ok { 977 for k, v := range cfgMap { 978 p := k 979 if path != "" { 980 p = path + "." + k 981 } 982 flatten(p, v, out) 983 } 984 } else { 985 out[path] = cfg 986 } 987 } 988 989 // SystemDefaults returns default system configuration from gadget defaults. 990 func SystemDefaults(gadgetDefaults map[string]map[string]interface{}) map[string]interface{} { 991 for _, systemSnap := range []string{"system", naming.WellKnownSnapID("core")} { 992 if defaults, ok := gadgetDefaults[systemSnap]; ok { 993 coreDefaults := map[string]interface{}{} 994 flatten("", defaults, coreDefaults) 995 return coreDefaults 996 } 997 } 998 return nil 999 } 1000 1001 // See https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html 1002 var disallowedKernelArguments = []string{ 1003 "root", "nfsroot", 1004 "init", 1005 } 1006 1007 // isKernelArgumentAllowed checks whether the kernel command line argument is 1008 // allowed. Prohibits all arguments listed explicitly in 1009 // disallowedKernelArguments list and those prefixed with snapd, with exception 1010 // of snapd.debug. All other arguments are allowed. 1011 func isKernelArgumentAllowed(arg string) bool { 1012 if strings.HasPrefix(arg, "snapd") && arg != "snapd.debug" { 1013 return false 1014 } 1015 if strutil.ListContains(disallowedKernelArguments, arg) { 1016 return false 1017 } 1018 return true 1019 } 1020 1021 var ErrNoKernelCommandline = errors.New("no kernel command line in the gadget") 1022 1023 // KernelCommandLineFromGadget returns the desired kernel command line provided by the 1024 // gadget. The full flag indicates whether the gadget provides a full command 1025 // line or just the extra parameters that will be appended to the static ones. 1026 // An ErrNoKernelCommandline is returned when thea gadget does not set any 1027 // kernel command line. 1028 func KernelCommandLineFromGadget(gadgetDirOrSnapPath string) (cmdline string, full bool, err error) { 1029 sf, err := snapfile.Open(gadgetDirOrSnapPath) 1030 if err != nil { 1031 return "", false, fmt.Errorf("cannot open gadget snap: %v", err) 1032 } 1033 contentExtra, err := sf.ReadFile("cmdline.extra") 1034 if err != nil && !os.IsNotExist(err) { 1035 return "", false, err 1036 } 1037 contentFull, err := sf.ReadFile("cmdline.full") 1038 if err != nil && !os.IsNotExist(err) { 1039 return "", false, err 1040 } 1041 content := contentExtra 1042 whichFile := "cmdline.extra" 1043 switch { 1044 case contentExtra != nil && contentFull != nil: 1045 return "", false, fmt.Errorf("cannot support both extra and full kernel command lines") 1046 case contentExtra == nil && contentFull == nil: 1047 return "", false, ErrNoKernelCommandline 1048 case contentFull != nil: 1049 content = contentFull 1050 whichFile = "cmdline.full" 1051 full = true 1052 } 1053 parsed, err := parseCommandLineFromGadget(content, whichFile) 1054 if err != nil { 1055 return "", full, fmt.Errorf("invalid kernel command line in %v: %v", whichFile, err) 1056 } 1057 return parsed, full, nil 1058 } 1059 1060 // parseCommandLineFromGadget parses the command line file and returns a 1061 // reassembled kernel command line as a single string. The file can be multi 1062 // line, where only lines stating with # are treated as comments, eg. 1063 // 1064 // foo 1065 // # this is a comment 1066 // 1067 // According to https://elixir.bootlin.com/linux/latest/source/Documentation/admin-guide/kernel-parameters.txt 1068 // the # character can appear as part of a valid kernel command line argument, 1069 // specifically in the following argument: 1070 // memmap=nn[KMG]#ss[KMG] 1071 // memmap=100M@2G,100M#3G,1G!1024G 1072 // Thus a lone # or a token starting with # are treated as errors. 1073 func parseCommandLineFromGadget(content []byte, whichFile string) (string, error) { 1074 s := bufio.NewScanner(bytes.NewBuffer(content)) 1075 filtered := &bytes.Buffer{} 1076 for s.Scan() { 1077 line := s.Text() 1078 if len(line) > 0 && line[0] == '#' { 1079 // comment 1080 continue 1081 } 1082 filtered.WriteRune(' ') 1083 filtered.WriteString(line) 1084 } 1085 if err := s.Err(); err != nil { 1086 return "", err 1087 } 1088 kargs, err := osutil.KernelCommandLineSplit(filtered.String()) 1089 if err != nil { 1090 return "", err 1091 } 1092 for _, argValue := range kargs { 1093 if strings.HasPrefix(argValue, "#") { 1094 return "", fmt.Errorf("unexpected or invalid use of # in argument %q", argValue) 1095 } 1096 split := strings.SplitN(argValue, "=", 2) 1097 if !isKernelArgumentAllowed(split[0]) { 1098 return "", fmt.Errorf("disallowed kernel argument %q", argValue) 1099 } 1100 } 1101 return strings.Join(kargs, " "), nil 1102 }