sys-apps/ignition: Patch to address partitioning race conditions

Signed-off-by: James Le Cuirot <jlecuirot@microsoft.com>
This commit is contained in:
James Le Cuirot 2026-05-11 14:59:13 +01:00 committed by Krzesimir Nowak
parent c580830d40
commit 3ff790c37f
4 changed files with 211 additions and 0 deletions

View File

@ -0,0 +1,108 @@
From 31f898a9eef2fdf83683b0f5d9551d45e669223d Mon Sep 17 00:00:00 2001
From: James Le Cuirot <jlecuirot@microsoft.com>
Date: Mon, 11 May 2026 12:25:38 +0100
Subject: [PATCH 1/2] stages/disks: Make getRealStartAndSize return a map like
it says it does
This is useful for the code I'm about to add. Use int rather than uint64
because that's what sgdisk.Partition.Number uses. That should be more
than big enough!
Signed-off-by: James Le Cuirot <jlecuirot@microsoft.com>
---
internal/exec/stages/disks/partitions.go | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go
index 9a7ae9ca..469ba77f 100644
--- a/internal/exec/stages/disks/partitions.go
+++ b/internal/exec/stages/disks/partitions.go
@@ -133,7 +133,7 @@ func convertMiBToSectors(mib *int, sectorSize int) *int64 {
// getRealStartAndSize returns a map of partition numbers to a struct that contains what their real start
// and end sector should be. It runs sgdisk --pretend to determine what the partitions would look like if
// everything specified were to be (re)created.
-func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]sgdisk.Partition, error) {
+func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) (map[int]sgdisk.Partition, error) {
partitions := []sgdisk.Partition{}
for _, cpart := range dev.Partitions {
partitions = append(partitions, sgdisk.Partition{
@@ -182,7 +182,7 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti
return nil, err
}
- result := []sgdisk.Partition{}
+ result := map[int]sgdisk.Partition{}
for _, part := range partitions {
if dims, ok := realDimensions[part.Number]; ok {
if part.StartSector != nil {
@@ -192,7 +192,7 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti
part.SizeInSectors = &dims.size
}
}
- result = append(result, part)
+ result[part.Number] = part
}
return result, nil
}
@@ -486,9 +486,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
return err
}
- var partxAdd []uint64
- var partxDelete []uint64
- var partxUpdate []uint64
+ var partxAdd []int
+ var partxDelete []int
+ var partxUpdate []int
for _, part := range resolvedPartitions {
shouldExist := partitionShouldExist(part)
@@ -510,13 +510,13 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
case !exists && shouldExist:
op.CreatePartition(part)
modification = true
- partxAdd = append(partxAdd, uint64(part.Number))
+ partxAdd = append(partxAdd, part.Number)
case exists && !shouldExist && !wipeEntry:
return fmt.Errorf("partition %d exists but is specified as nonexistant and wipePartitionEntry is false", part.Number)
case exists && !shouldExist && wipeEntry:
op.DeletePartition(part.Number)
modification = true
- partxDelete = append(partxDelete, uint64(part.Number))
+ partxDelete = append(partxDelete, part.Number)
case exists && shouldExist && matches:
s.Info("partition %d found with correct specifications", part.Number)
case exists && shouldExist && !wipeEntry && !matches:
@@ -530,7 +530,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
part.StartSector = &info.StartSector
op.CreatePartition(part)
modification = true
- partxUpdate = append(partxUpdate, uint64(part.Number))
+ partxUpdate = append(partxUpdate, part.Number)
} else {
return fmt.Errorf("partition %d didn't match: %v", part.Number, matchErr)
}
@@ -539,7 +539,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
op.DeletePartition(part.Number)
op.CreatePartition(part)
modification = true
- partxUpdate = append(partxUpdate, uint64(part.Number))
+ partxUpdate = append(partxUpdate, part.Number)
default:
// unfortunatey, golang doesn't check that all cases are handled exhaustively
return fmt.Errorf("unreachable code reached when processing partition %d. golang--", part.Number)
@@ -558,9 +558,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
// kernel partition table with BLKPG but only uses BLKRRPART which fails
// as soon as one partition of the disk is mounted
if len(activeParts) > 0 {
- runPartxCommand := func(op string, partitions []uint64) error {
+ runPartxCommand := func(op string, partitions []int) error {
for _, partNr := range partitions {
- cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", strconv.FormatUint(partNr, 10), blockDevResolved)
+ cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", fmt.Sprint(partNr), blockDevResolved)
if _, err := s.LogCmd(cmd, "triggering partition %d %s on %q", partNr, op, devAlias); err != nil {
return fmt.Errorf("partition %s failed: %v", op, err)
}
--
2.53.0

View File

@ -0,0 +1,101 @@
From 305f9601c8968a993eaefc5ed251fa8ead26140a Mon Sep 17 00:00:00 2001
From: James Le Cuirot <jlecuirot@microsoft.com>
Date: Fri, 8 May 2026 17:58:38 +0100
Subject: [PATCH 2/2] stages/disks: Allow partx to fail then check the state
later
`partx --add` will fail if the kernel is already aware of the new
partition. It was always theoretically possible that udev might trigger
early, and that appears to be happening now.
Allow partx to fail and then check that added/updated partitions have
the right start sector and size and that deleted partitions are absent
once udev has settled.
Signed-off-by: James Le Cuirot <jlecuirot@microsoft.com>
---
internal/exec/stages/disks/partitions.go | 62 ++++++++++++++++++++----
1 file changed, 53 insertions(+), 9 deletions(-)
diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go
index 469ba77f..8b7d859b 100644
--- a/internal/exec/stages/disks/partitions.go
+++ b/internal/exec/stages/disks/partitions.go
@@ -567,15 +567,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
}
return nil
}
- if err := runPartxCommand("delete", partxDelete); err != nil {
- return err
- }
- if err := runPartxCommand("update", partxUpdate); err != nil {
- return err
- }
- if err := runPartxCommand("add", partxAdd); err != nil {
- return err
- }
+ runPartxCommand("delete", partxDelete)
+ runPartxCommand("update", partxUpdate)
+ runPartxCommand("add", partxAdd)
}
// It's best to wait here for the /dev/ABC entries to be
@@ -586,5 +580,55 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
return fmt.Errorf("failed to wait for udev on %q after partitioning: %v", devAlias, err)
}
+ for _, partNum := range append(append([]int{}, partxAdd...), partxUpdate...) {
+ part := resolvedPartitions[partNum]
+ partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, partNum)
+ sysBlockDir := fmt.Sprintf("/sys/class/block/%s/", filepath.Base(partDev))
+
+ // sysfs always reports in 512-byte sectors; convert our expected
+ // values from logical sectors to 512-byte sectors for comparison
+ logicalTo512 := int64(diskInfo.LogicalSectorSize) / 512
+
+ startStr, err := os.ReadFile(sysBlockDir + "start")
+ if err != nil {
+ return fmt.Errorf("failed to read start of %q from sysfs: %v", partDev, err)
+ }
+ kernelStart, err := strconv.ParseInt(strings.TrimSpace(string(startStr)), 10, 64)
+ if err != nil {
+ return fmt.Errorf("failed to parse start of %q from sysfs: %v", partDev, err)
+ }
+ if part.StartSector != nil {
+ expectedStart := *part.StartSector * logicalTo512
+ if kernelStart != expectedStart {
+ return fmt.Errorf("kernel partition start for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelStart, expectedStart)
+ }
+ }
+
+ sizeStr, err := os.ReadFile(sysBlockDir + "size")
+ if err != nil {
+ return fmt.Errorf("failed to read size of %q from sysfs: %v", partDev, err)
+ }
+ kernelSize, err := strconv.ParseInt(strings.TrimSpace(string(sizeStr)), 10, 64)
+ if err != nil {
+ return fmt.Errorf("failed to parse size of %q from sysfs: %v", partDev, err)
+ }
+ if part.SizeInSectors != nil {
+ expectedSize := *part.SizeInSectors * logicalTo512
+ if kernelSize != expectedSize {
+ return fmt.Errorf("kernel partition size for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelSize, expectedSize)
+ }
+ }
+ }
+
+ for _, partNum := range partxDelete {
+ partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, partNum)
+ _, err := os.Stat(partDev)
+ if err == nil {
+ return fmt.Errorf("%q unexpectedly exists after partitioning", partDev)
+ } else if !os.IsNotExist(err) {
+ return fmt.Errorf("failed to stat %q after partitioning: %v", partDev, err)
+ }
+ }
+
return nil
}
--
2.53.0

View File

@ -35,6 +35,8 @@ else
"${FILESDIR}/0017-docs-Add-re-added-platforms-to-docs-to-pass-tests.patch"
"${FILESDIR}/0018-usr-share-oem-oem.patch"
"${FILESDIR}/0019-internal-exec-stages-mount-Mount-oem.patch"
"${FILESDIR}/0019-stages-disks-Make-getRealStartAndSize-return-a-map-l.patch"
"${FILESDIR}/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch"
)
fi