Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: kernel partition sync when overwriting GPT #115

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions partitioning/gpt/gpt.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type Table struct {

alignment uint64
sectorSize uint

// newTable is true if the table completely overwrites existing partitions.
newTable bool
}

// Partition is a single partition entry in GPT.
Expand Down Expand Up @@ -128,6 +131,7 @@ func New(dev Device, opts ...Option) (*Table, error) {
dev: dev,
options: options,
diskGUID: diskGUID,
newTable: true,
}

t.init(lastLBA)
Expand Down Expand Up @@ -267,6 +271,7 @@ func (t *Table) init(lastLBA uint64) {
// Clear the partition table.
func (t *Table) Clear() {
t.entries = nil
t.newTable = true
}

// Compact the partition table by removing empty entries.
Expand Down Expand Up @@ -600,6 +605,52 @@ func (t *Table) writePMBR() error {
}

func (t *Table) syncKernel() error {
if t.newTable {
return t.syncKernelComplete()
}

return t.syncKernelIncremental()
}

// syncKernelComplete synchronizes the kernel partition table with the current table by overwriting the whole table.
//
// It is incompatible with mounted partitions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't there a syscall to just tell the kernel to re-read the partition table? Also we must probably check this is not running on a disk which has a mounted partition and return an error if so

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that if something calls gpt.New vs. gpt.Read then we know that whatever existing partitions are going to be removed.

The call would fail if the mounted partitions are there, but it's not the job of this library to handle that (it should be handled in the layer up).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And by that syscall I meant BLKRRPART to be clear

func (t *Table) syncKernelComplete() error {
kernelPartitionNum, err := t.dev.GetKernelLastPartitionNum()
if err != nil {
return fmt.Errorf("failed to get kernel last partition number: %w", err)
}

// delete all kernel partitions
for no := 1; no <= kernelPartitionNum; no++ {
if err := t.dev.KernelPartitionDelete(no); err != nil && !errors.Is(err, unix.ENXIO) {
return fmt.Errorf("failed to delete partition %d: %w", no, err)
}
}

// re-create all partitions
for no := 1; no <= len(t.entries); no++ {
myEntry := t.entries[no-1]

if myEntry == nil {
continue
}

if err := t.dev.KernelPartitionAdd(no,
myEntry.FirstLBA*uint64(t.sectorSize),
(myEntry.LastLBA-myEntry.FirstLBA+1)*uint64(t.sectorSize),
); err != nil {
return fmt.Errorf("failed to add partition %d: %w", no, err)
}
}

return nil
}

// syncKernelIncremental synchronizes the kernel partition table with the current table by doing minimal changes.
//
// It allows to change live partition layout, while some partitions are mounted.
func (t *Table) syncKernelIncremental() error {
kernelPartitionNum, err := t.dev.GetKernelLastPartitionNum()
if err != nil {
return fmt.Errorf("failed to get kernel last partition number: %w", err)
Expand Down
69 changes: 69 additions & 0 deletions partitioning/gpt/gpt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,75 @@ func TestGPT(t *testing.T) {
}
}

func TestGPTOverwrite(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root privileges")
}

if hostname, _ := os.Hostname(); hostname == "buildkitsandbox" { //nolint: errcheck
t.Skip("test not supported under buildkit as partition devices are not propagated from /dev")
}

partType1 := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
partType2 := uuid.MustParse("E6D6D379-F507-44C2-A23C-238F2A3DF928")

// create a partition table, and then overwrite it with a new one with incompatible layout
tmpDir := t.TempDir()

rawImage := filepath.Join(tmpDir, "image.raw")

f, err := os.Create(rawImage)
require.NoError(t, err)

require.NoError(t, f.Truncate(int64(3*GiB)))
require.NoError(t, f.Close())

loDev := losetupAttachHelper(t, rawImage, false)

t.Cleanup(func() {
assert.NoError(t, loDev.Detach())
})

disk, err := os.OpenFile(loDev.Path(), os.O_RDWR, 0)
require.NoError(t, err)

t.Cleanup(func() {
assert.NoError(t, disk.Close())
})

blkdev := block.NewFromFile(disk)

gptdev, err := gpt.DeviceFromBlockDevice(blkdev)
require.NoError(t, err)

table, err := gpt.New(gptdev)
require.NoError(t, err)

// allocate 2 1G partitions first
require.NoError(t, allocateError(table.AllocatePartition(100*MiB, "1G", partType1)))
require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "2G", partType2)))

require.NoError(t, table.Write())

assert.FileExists(t, loDev.Path()+"p1")
assert.FileExists(t, loDev.Path()+"p2")

// now attempt to overwrite the partition table with a new one with different layout
table2, err := gpt.New(gptdev)
require.NoError(t, err)

// allocate new partitions first
require.NoError(t, allocateError(table2.AllocatePartition(600*MiB, "1P", partType1)))
require.NoError(t, allocateError(table2.AllocatePartition(600*MiB, "2P", partType2)))
require.NoError(t, allocateError(table2.AllocatePartition(600*MiB, "3P", partType2)))

require.NoError(t, table2.Write())

assert.FileExists(t, loDev.Path()+"p1")
assert.FileExists(t, loDev.Path()+"p2")
assert.FileExists(t, loDev.Path()+"p3")
}

func losetupAttachHelper(t *testing.T, rawImage string, readonly bool) losetup.Device {
t.Helper()

Expand Down