From 34a912c4b9b80338ddb731e99b78f7f1545741ff Mon Sep 17 00:00:00 2001 From: Philip Meulengracht Date: Tue, 24 Sep 2024 09:30:15 +0200 Subject: [PATCH 1/2] gadget: support new namings in gadget validation for EMMC schema and content --- gadget/validate.go | 19 +++++++++++++++++++ gadget/validate_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/gadget/validate.go b/gadget/validate.go index 5fb5576f416..457dcee2990 100644 --- a/gadget/validate.go +++ b/gadget/validate.go @@ -112,6 +112,9 @@ func ruleValidateVolumes(vols map[string]*Volume, model Model, extra *Validation if err := ruleValidateVolume(v, hasModes); err != nil { return fmt.Errorf("invalid volume %q: %v", name, err) } + if v.Schema == schemaEMMC && len(vols) > 2 { + return fmt.Errorf(`cannot use "schema: emmc" with multiple volumes yet`) + } } if isClassicWithModes { @@ -150,6 +153,16 @@ func ruleValidateVolume(vol *Volume, hasModes bool) error { return nil } +func validateEMMCVolumeNames(vs *VolumeStructure) error { + if vs.EnclosingVolume.Schema != schemaEMMC { + return nil + } + if !strutil.ListContains(validEMMCVolumeNames, vs.Name) { + return fmt.Errorf("cannot use %q as emmc name, only %q is allowed", vs.Name, validEMMCVolumeNames) + } + return nil +} + func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error { var reservedLabels []string if hasModes { @@ -160,6 +173,9 @@ func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error { if err := validateReservedLabels(vs, reservedLabels); err != nil { return err } + if err := validateEMMCVolumeNames(vs); err != nil { + return err + } return nil } @@ -179,6 +195,9 @@ var ( ubuntuSeedLabel, ubuntuDataLabel, } + + // valid names for volumes under an eMMC schema + validEMMCVolumeNames = []string{"boot0", "boot1", "rpmb"} ) func validateReservedLabels(vs *VolumeStructure, reservedLabels []string) error { diff --git a/gadget/validate_test.go b/gadget/validate_test.go index 2e92e0dd098..785fc861d06 100644 --- a/gadget/validate_test.go +++ b/gadget/validate_test.go @@ -78,6 +78,7 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C) }, }, } + gadget.SetEnclosingVolumeInStructs(gi.Volumes) err := gadget.Validate(gi, tc.model, nil) if tc.err == "" { c.Check(err, IsNil) @@ -88,6 +89,36 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C) } +func (s *validateGadgetTestSuite) TestRuleValidateStructureEmmcNames(c *C) { + for _, tc := range []struct { + name, err string + model gadget.Model + }{ + {name: "some-name", err: `cannot use "some-name" as emmc name, only \["boot0" "boot1" "rpmb"\] is allowed`}, + {name: "boot0", err: ""}, + {name: "boot1", err: ""}, + {name: "rpmb", err: ""}, + } { + gi := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "emmc": { + Schema: "emmc", + Structure: []gadget.VolumeStructure{{ + Name: tc.name, + }}, + }, + }, + } + gadget.SetEnclosingVolumeInStructs(gi.Volumes) + err := gadget.Validate(gi, tc.model, nil) + if tc.err == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, ".*: "+tc.err) + } + } +} + // rolesYaml produces gadget metadata with volumes with structure withs the given // role if data, seed or save are != "-", and with their label set to the value func rolesYaml(c *C, data, seed, save string) *gadget.Info { From fb019511089687b1b0b18611c4abfec30df68d66 Mon Sep 17 00:00:00 2001 From: Philip Meulengracht Date: Tue, 24 Sep 2024 09:49:00 +0200 Subject: [PATCH 2/2] gadget: add support for new schema --- gadget/gadget.go | 130 +++++++++---- gadget/gadget_emmc_test.go | 380 +++++++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+), 35 deletions(-) create mode 100644 gadget/gadget_emmc_test.go diff --git a/gadget/gadget.go b/gadget/gadget.go index 05d226fbc5b..7d772623044 100644 --- a/gadget/gadget.go +++ b/gadget/gadget.go @@ -55,6 +55,8 @@ const ( schemaMBR = "mbr" // schemaGPT identifies a GUID Partition Table partitioning schema schemaGPT = "gpt" + // schemaEMMC identifies a schema for eMMC + schemaEMMC = "emmc" SystemBoot = "system-boot" SystemData = "system-data" @@ -1088,6 +1090,24 @@ func asOffsetPtr(offs quantity.Offset) *quantity.Offset { return &offs } +func setVolumeStructureOffset(vs *VolumeStructure, startPtr *quantity.Offset) (next *quantity.Offset) { + if vs.Offset == nil && startPtr != nil { + var start quantity.Offset + if vs.Role != schemaMBR && *startPtr < NonMBRStartOffset { + start = NonMBRStartOffset + } else { + start = *startPtr + } + vs.Offset = &start + } + // We know the end of the structure only if we could define an offset + // and the size is fixed. + if vs.Offset != nil && vs.isFixedSize() { + return asOffsetPtr(*vs.Offset + quantity.Offset(vs.Size)) + } + return nil +} + func setImplicitForVolume(vol *Volume, model Model) error { rs := whichVolRuleset(model) if vol.HasPartial(PartialSchema) { @@ -1112,42 +1132,34 @@ func setImplicitForVolume(vol *Volume, model Model) error { previousEnd := asOffsetPtr(0) for i := range vol.Structure { + vs := &vol.Structure[i] + // set the VolumeName for the structure from the volume itself - vol.Structure[i].VolumeName = vol.Name + vs.VolumeName = vol.Name // Store index as we will reorder later - vol.Structure[i].YamlIndex = i + vs.YamlIndex = i // MinSize is Size if not explicitly set - if vol.Structure[i].MinSize == 0 { - vol.Structure[i].MinSize = vol.Structure[i].Size + if vs.MinSize == 0 { + vs.MinSize = vs.Size } // Set the pointer to the volume - vol.Structure[i].EnclosingVolume = vol + vs.EnclosingVolume = vol // set other implicit data for the structure - if err := setImplicitForVolumeStructure(&vol.Structure[i], rs, knownFsLabels, knownVfatFsLabels); err != nil { + if err := setImplicitForVolumeStructure(vs, rs, knownFsLabels, knownVfatFsLabels); err != nil { return err } // Set offset if it was not set (must be after setImplicitForVolumeStructure // so roles are good). This is possible only if the previous structure had // a well-defined end. - if vol.Structure[i].Offset == nil && previousEnd != nil { - var start quantity.Offset - if vol.Structure[i].Role != schemaMBR && *previousEnd < NonMBRStartOffset { - start = NonMBRStartOffset - } else { - start = *previousEnd - } - vol.Structure[i].Offset = &start - } - // We know the end of the structure only if we could define an offset - // and the size is fixed. - if vol.Structure[i].Offset != nil && vol.Structure[i].isFixedSize() { - previousEnd = asOffsetPtr(*vol.Structure[i].Offset + - quantity.Offset(vol.Structure[i].Size)) - } else { - previousEnd = nil + if vol.Schema == schemaEMMC { + // For eMMC, we do not support partition offsets. The partitions + // are hardware partitions that act more like traditional disks. + vs.Offset = asOffsetPtr(0) + continue } + previousEnd = setVolumeStructureOffset(vs, previousEnd) } return nil @@ -1267,11 +1279,17 @@ func fmtIndexAndName(idx int, name string) string { return fmt.Sprintf("#%v", idx) } +var validSchemaNames = []string{schemaMBR, schemaGPT, schemaEMMC} + +func isValidSchema(schema string) bool { + return strutil.ListContains(validSchemaNames, schema) +} + func validateVolume(vol *Volume) error { if !validVolumeName.MatchString(vol.Name) { return errors.New("invalid name") } - if !vol.HasPartial(PartialSchema) && vol.Schema != schemaGPT && vol.Schema != schemaMBR { + if !vol.HasPartial(PartialSchema) && !isValidSchema(vol.Schema) { return fmt.Errorf("invalid schema %q", vol.Schema) } @@ -1338,6 +1356,12 @@ func isMBR(vs *VolumeStructure) bool { } func validateCrossVolumeStructure(vol *Volume) error { + // emmc have no traditional volumes, instead emmc has the concept + // of hardware partitions that act like separate disks. + if vol.Schema == schemaEMMC { + return nil + } + previousEnd := quantity.Offset(0) // cross structure validation: // - relative offsets that reference other structures by name @@ -1393,6 +1417,20 @@ func validateOffsetWrite(s, firstStruct *VolumeStructure, volSize quantity.Size) return nil } +func contentCheckerCreate(vs *VolumeStructure, vol *Volume) func(string, *VolumeContent) error { + if vol.Schema == schemaEMMC { + return validateEMMCContent + } + + if vs.HasFilesystem() { + return validateFilesystemContent + } + + // default to bare content checker if no filesystem + // is present + return validateBareContent +} + func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { if !vs.hasPartialSize() { if vs.Size == 0 { @@ -1419,20 +1457,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { return fmt.Errorf("invalid filesystem %q", vs.Filesystem) } - var contentChecker func(*VolumeContent) error - - if vs.HasFilesystem() { - contentChecker = validateFilesystemContent - } else { - contentChecker = validateBareContent - } + contentChecker := contentCheckerCreate(vs, vol) for i, c := range vs.Content { - if err := contentChecker(&c); err != nil { + if err := contentChecker(vs.Name, &c); err != nil { return fmt.Errorf("invalid content #%v: %v", i, err) } } - if err := validateStructureUpdate(vs, vol); err != nil { + if err := validateStructureUpdate(vs); err != nil { return err } @@ -1442,6 +1474,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { return nil } +func validateStructureTypeEMMC(s string) error { + // for eMMC we don't support the type being set + if s != "" { + return errors.New(`type is not supported for "emmc" schema`) + } + return nil +} + func validateStructureType(s string, vol *Volume) error { // Type can be one of: // - "mbr" (backwards compatible) @@ -1453,6 +1493,11 @@ func validateStructureType(s string, vol *Volume) error { // Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID // example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + // eMMC volumes we treat differently + if vol.Schema == schemaEMMC { + return validateStructureTypeEMMC(s) + } + if s == "" { return errors.New(`type is not specified`) } @@ -1552,7 +1597,7 @@ func validateRole(vs *VolumeStructure) error { return nil } -func validateBareContent(vc *VolumeContent) error { +func validateBareContent(_ string, vc *VolumeContent) error { if vc.UnresolvedSource != "" || vc.Target != "" { return fmt.Errorf("cannot use non-image content for bare file system") } @@ -1562,7 +1607,22 @@ func validateBareContent(vc *VolumeContent) error { return nil } -func validateFilesystemContent(vc *VolumeContent) error { +func validateEMMCContent(name string, vc *VolumeContent) error { + // We only allow offset and size for the rpmb block + if (vc.Offset != nil || vc.Size != 0) && name != "rpmb" { + return fmt.Errorf("cannot specify size or offset for content in %q", name) + } + + if vc.UnresolvedSource != "" || vc.Target != "" { + return fmt.Errorf("cannot use non-image content for hardware partitions") + } + if vc.Image == "" { + return fmt.Errorf("missing image file name") + } + return nil +} + +func validateFilesystemContent(_ string, vc *VolumeContent) error { if vc.Image != "" || vc.Offset != nil || vc.Size != 0 { return fmt.Errorf("cannot use image content for non-bare file system") } @@ -1575,7 +1635,7 @@ func validateFilesystemContent(vc *VolumeContent) error { return nil } -func validateStructureUpdate(vs *VolumeStructure, v *Volume) error { +func validateStructureUpdate(vs *VolumeStructure) error { if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 { return errors.New("preserving files during update is not supported for non-filesystem structures") } diff --git a/gadget/gadget_emmc_test.go b/gadget/gadget_emmc_test.go new file mode 100644 index 00000000000..80718ca851a --- /dev/null +++ b/gadget/gadget_emmc_test.go @@ -0,0 +1,380 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package gadget_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/testutil" +) + +type gadgetYamlEMMCSuite struct { + testutil.BaseTest + + dir string + gadgetYamlPath string +} + +var _ = Suite(&gadgetYamlEMMCSuite{}) + +var mockEMMCGadgetYaml = []byte(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + - source: foo + target: / + my-emmc: + schema: emmc + structure: + - name: boot0 + size: 4M + content: + - image: boot0filename + - name: boot1 + size: 4M + content: + - image: boot1filename + - name: rpmb + size: 131072 + content: + - image: rpmbfilename + offset: 1234 + size: 4321 +`) + +var mockEMMCMultiVolumeGadgetYaml = string(mockEMMCGadgetYaml) + ` + other-volume: + schema: mbr + id: 0C + structure: + - filesystem-label: data + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat +` + +func (s *gadgetYamlEMMCSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + s.dir = c.MkDir() + c.Assert(os.MkdirAll(filepath.Join(s.dir, "meta"), 0755), IsNil) + s.gadgetYamlPath = filepath.Join(s.dir, "meta", "gadget.yaml") +} + +func (s *gadgetYamlEMMCSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlMultiVolumeEMMCNotSupported(c *C) { + err := os.WriteFile(s.gadgetYamlPath, []byte(mockEMMCMultiVolumeGadgetYaml), 0644) + c.Assert(err, IsNil) + + info, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + + err = gadget.Validate(info, nil, nil) + c.Assert(err, ErrorMatches, `cannot use "schema: emmc" with multiple volumes yet`) +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlOffsetNotSupportedForBoot(c *C) { + for _, t := range []string{"boot0", "boot1"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - image: boot0filename + offset: 1000 +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, fmt.Sprintf(`.*cannot specify size or offset for content in %q`, t)) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlSourceIsNotSupported(c *C) { + for _, t := range []string{"boot0", "boot1", "rpmb"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - source: hello.bin +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, `.*cannot use non-image content for hardware partitions`) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlImageMustBeSet(c *C) { + for _, t := range []string{"boot0", "boot1", "rpmb"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - unpack: true +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, `.*missing image file name`) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlHappy(c *C) { + err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644) + c.Assert(err, IsNil) + + ginfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + expected := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "volumename": { + Name: "volumename", + Schema: "mbr", + Bootloader: "u-boot", + ID: "0C", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "volumename", + Label: "system-boot", + Role: "system-boot", // implicit + Offset: asOffsetPtr(12345), + OffsetWrite: mustParseGadgetRelativeOffset(c, "777"), + Size: 88888, + MinSize: 88888, + Type: "0C", + Filesystem: "vfat", + Content: []gadget.VolumeContent{ + { + UnresolvedSource: "subdir/", + Target: "/", + Unpack: false, + }, + { + UnresolvedSource: "foo", + Target: "/", + Unpack: false, + }, + }, + }, + }, + }, + "my-emmc": { + Name: "my-emmc", + Schema: "emmc", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "my-emmc", + Name: "boot0", + Offset: asOffsetPtr(0), + Size: 4 * 1024 * 1024, + MinSize: 4 * 1024 * 1024, + Content: []gadget.VolumeContent{ + { + Image: "boot0filename", + }, + }, + YamlIndex: 0, + }, { + VolumeName: "my-emmc", + Name: "boot1", + Offset: asOffsetPtr(0), + Size: 4 * 1024 * 1024, + MinSize: 4 * 1024 * 1024, + Content: []gadget.VolumeContent{ + { + Image: "boot1filename", + }, + }, + YamlIndex: 1, + }, { + VolumeName: "my-emmc", + Name: "rpmb", + Offset: asOffsetPtr(0), + Size: 131072, + MinSize: 131072, + Content: []gadget.VolumeContent{ + { + Image: "rpmbfilename", + Offset: asOffsetPtr(1234), + Size: 4321, + }, + }, + YamlIndex: 2, + }, + }, + }, + }, + } + gadget.SetEnclosingVolumeInStructs(expected.Volumes) + + c.Check(ginfo, DeepEquals, expected) +} + +func (s *gadgetYamlEMMCSuite) TestUpdateApplyHappy(c *C) { + err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644) + c.Assert(err, IsNil) + + oldInfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "boot0filename"), 1*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "boot1filename"), 1*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "rpmbfilename"), 1234, nil) + oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newInfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + // pretend we have an update + newInfo.Volumes["my-emmc"].Structure[1].Update.Edition = 1 + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "boot0filename"), 1*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "boot1filename"), 2*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "rpmbfilename"), 1234, nil) + newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir := c.MkDir() + + restore := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, gm gadget.Model, gv map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + return map[string]map[int]gadget.StructureLocation{ + "volumename": { + 0: { + Device: "/dev/emmcblk0", + Offset: quantity.OffsetMiB, + RootMountPoint: "/run/mnt/ubuntu-boot", + }, + }, + "my-emmc": { + 0: { + Device: "/dev/emmcblk0boot0", + }, + 1: { + Device: "/dev/emmcblk0boot1", + }, + 2: { + Device: "/dev/emmcblk0rpmb", + }, + }, + }, map[string]map[int]*gadget.OnDiskStructure{ + "volumename": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volumename"]), + "my-emmc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["my-emmc"]), + }, nil + }) + defer restore() + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + fmt.Println("update-for-structure", loc, ps, fromPs) + updaterForStructureCalls++ + mu := &mockUpdater{} + + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(updaterForStructureCalls, Equals, 1) +}