From 114af20196847618265af39862b434021f488e25 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Fri, 2 Aug 2024 19:26:42 +0400 Subject: [PATCH] feat: add device wipe and partition devname Device wipe mostly copied from v1 library with some fixes. Partition devname was in Talos PR, but it should be better here. Signed-off-by: Andrey Smirnov --- block/device.go | 20 ++++++ block/device_linux.go | 10 ++- block/wipe_linux.go | 108 ++++++++++++++++++++++++++++++++ block/wipe_linux_test.go | 101 +++++++++++++++++++++++++++++ partitioning/gpt/gpt.go | 4 +- partitioning/gpt/gpt_test.go | 2 +- partitioning/partioning_test.go | 43 +++++++++++++ partitioning/partitioning.go | 19 ++++++ 8 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 block/wipe_linux.go create mode 100644 block/wipe_linux_test.go create mode 100644 partitioning/partioning_test.go create mode 100644 partitioning/partitioning.go diff --git a/block/device.go b/block/device.go index ece310f..ec69d82 100644 --- a/block/device.go +++ b/block/device.go @@ -31,6 +31,11 @@ func (d *Device) Close() error { return nil } +// File returns the underlying file. +func (d *Device) File() *os.File { + return d.f +} + // DefaultBlockSize is the default block size in bytes. const DefaultBlockSize = 512 @@ -56,3 +61,18 @@ type DeviceProperties struct { // Rotational is true if the device is a rotational disk. Rotational bool } + +// Options for NewFromPath. +type Options struct { + Flag int +} + +// Option is a function that modifies Options. +type Option func(*Options) + +// OpenForWrite opens the device for writing. +func OpenForWrite() Option { + return func(o *Options) { + o.Flag |= os.O_RDWR + } +} diff --git a/block/device_linux.go b/block/device_linux.go index b516462..08b22f9 100644 --- a/block/device_linux.go +++ b/block/device_linux.go @@ -19,8 +19,14 @@ import ( ) // NewFromPath returns a new Device from the specified path. -func NewFromPath(path string) (*Device, error) { - f, err := os.OpenFile(path, os.O_RDONLY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) +func NewFromPath(path string, opts ...Option) (*Device, error) { + var options Options + + for _, opt := range opts { + opt(&options) + } + + f, err := os.OpenFile(path, options.Flag|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return nil, err } diff --git a/block/wipe_linux.go b/block/wipe_linux.go new file mode 100644 index 0000000..72a6b16 --- /dev/null +++ b/block/wipe_linux.go @@ -0,0 +1,108 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package block + +import ( + "io" + "os" + "runtime" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + // FastWipeRange fast wipe block. + FastWipeRange = 1024 * 1024 +) + +// Wipe the device contents. +// +// In order of availability this tries to perform the following: +// - secure discard (secure erase) +// - discard with zeros +// - zero out via ioctl +// - zero out from userland +func (d *Device) Wipe() (string, error) { + size, err := d.GetSize() + if err != nil { + return "", err + } + + return d.WipeRange(0, size) +} + +// FastWipe the device contents. +// +// This method is much faster than Wipe(), but it doesn't guarantee +// that device will be zeroed out completely. +func (d *Device) FastWipe() error { + size, err := d.GetSize() + if err != nil { + return err + } + + // BLKDISCARD is implemented via TRIM on SSDs, it might or might not zero out device contents. + r := [2]uint64{0, size} + + // ignoring the error here as DISCARD might be not supported by the device + unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), unix.BLKDISCARD, uintptr(unsafe.Pointer(&r[0]))) //nolint: errcheck + + // zero out the first N bytes of the device to clear any partition table + wipeLength := min(size, uint64(FastWipeRange)) + + _, err = d.WipeRange(0, wipeLength) + if err != nil { + return err + } + + // wipe the last FastWipeRange bytes of the device as well + if size >= FastWipeRange*2 { + _, err = d.WipeRange(size-FastWipeRange, FastWipeRange) + if err != nil { + return err + } + } + + return nil +} + +// WipeRange the device [start, start+length). +func (d *Device) WipeRange(start, length uint64) (string, error) { + r := [2]uint64{start, length} + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), unix.BLKSECDISCARD, uintptr(unsafe.Pointer(&r[0]))); errno == 0 { + runtime.KeepAlive(d) + + return "blksecdiscard", nil + } + + var zeroes int + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), unix.BLKDISCARDZEROES, uintptr(unsafe.Pointer(&zeroes))); errno == 0 && zeroes != 0 { + if _, _, errno = unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), unix.BLKDISCARD, uintptr(unsafe.Pointer(&r[0]))); errno == 0 { + runtime.KeepAlive(d) + + return "blkdiscardzeros", nil + } + } + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), unix.BLKZEROOUT, uintptr(unsafe.Pointer(&r[0]))); errno == 0 { + runtime.KeepAlive(d) + + return "blkzeroout", nil + } + + zero, err := os.Open("/dev/zero") + if err != nil { + return "", err + } + + defer zero.Close() //nolint: errcheck + + _, err = io.CopyN(d.f, zero, int64(r[1])) + + return "writezeroes", err +} diff --git a/block/wipe_linux_test.go b/block/wipe_linux_test.go new file mode 100644 index 0000000..922b3b9 --- /dev/null +++ b/block/wipe_linux_test.go @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package block_test + +import ( + "crypto/rand" + "io" + "os" + "path/filepath" + "testing" + + "github.com/freddierice/go-losetup/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/go-blockdevice/v2/block" +) + +func TestDeviceWipe(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("skipping test; must be root") + } + + tmpDir := t.TempDir() + + rawImage := filepath.Join(tmpDir, "image.raw") + + f, err := os.Create(rawImage) + require.NoError(t, err) + + require.NoError(t, f.Truncate(int64(2*GiB))) + + t.Cleanup(func() { + require.NoError(t, f.Close()) + }) + + var loDev losetup.Device + + loDev, err = losetup.Attach(rawImage, 0, false) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, loDev.Detach()) + }) + + devPath := loDev.Path() + + devWhole, err := block.NewFromPath(devPath, block.OpenForWrite()) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, devWhole.Close()) + }) + + magic := make([]byte, 1024) + + _, err = io.ReadFull(rand.Reader, magic) + require.NoError(t, err) + + _, err = f.WriteAt(magic, 0) + require.NoError(t, err) + + _, err = f.WriteAt(magic, 10*MiB) + require.NoError(t, err) + + method, err := devWhole.Wipe() + require.NoError(t, err) + + t.Logf("wipe method: %s", method) + + assertZeroed(t, f, 0, 1024) + assertZeroed(t, f, 10*MiB, 1024) + + _, err = f.WriteAt(magic, 0) + require.NoError(t, err) + + _, err = f.WriteAt(magic, 2*GiB-1024) + require.NoError(t, err) + + require.NoError(t, devWhole.FastWipe()) + + assertZeroed(t, f, 0, 1024) + assertZeroed(t, f, 2*GiB-1024, 1024) +} + +func assertZeroed(t *testing.T, f *os.File, offset, length int64) { //nolint:unparam + t.Helper() + + buf := make([]byte, length) + + _, err := f.ReadAt(buf, offset) + require.NoError(t, err) + + for i, b := range buf { + if b != 0 { + t.Fatalf("expected zero at offset %d, got %d", offset+int64(i), b) + } + } +} diff --git a/partitioning/gpt/gpt.go b/partitioning/gpt/gpt.go index 9b72f60..c1313b6 100644 --- a/partitioning/gpt/gpt.go +++ b/partitioning/gpt/gpt.go @@ -90,14 +90,14 @@ func (wrapper *deviceWrapper) GetSize() uint64 { } // DeviceFromBlockDevice creates a new Device from a block.Device. -func DeviceFromBlockDevice(dev *block.Device, f *os.File) (Device, error) { +func DeviceFromBlockDevice(dev *block.Device) (Device, error) { size, err := dev.GetSize() if err != nil { return nil, err } return &deviceWrapper{ - File: f, + File: dev.File(), Device: dev, size: size, }, nil diff --git a/partitioning/gpt/gpt_test.go b/partitioning/gpt/gpt_test.go index 3eb5a8e..42f93e4 100644 --- a/partitioning/gpt/gpt_test.go +++ b/partitioning/gpt/gpt_test.go @@ -249,7 +249,7 @@ func TestGPT(t *testing.T) { blkdev := block.NewFromFile(disk) - gptdev, err := gpt.DeviceFromBlockDevice(blkdev, disk) + gptdev, err := gpt.DeviceFromBlockDevice(blkdev) require.NoError(t, err) table, err := gpt.New(gptdev, test.opts...) diff --git a/partitioning/partioning_test.go b/partitioning/partioning_test.go new file mode 100644 index 0000000..baaa544 --- /dev/null +++ b/partitioning/partioning_test.go @@ -0,0 +1,43 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package partitioning_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/go-blockdevice/v2/partitioning" +) + +func TestDevName(t *testing.T) { + t.Parallel() + + for _, test := range []struct { //nolint:govet + devname string + partition uint + + expected string + }{ + { + devname: "/dev/sda", + partition: 1, + + expected: "/dev/sda1", + }, + { + devname: "/dev/nvme0n1", + partition: 2, + + expected: "/dev/nvme0n1p2", + }, + } { + t.Run(test.devname, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, partitioning.DevName(test.devname, test.partition)) + }) + } +} diff --git a/partitioning/partitioning.go b/partitioning/partitioning.go new file mode 100644 index 0000000..2c2ff14 --- /dev/null +++ b/partitioning/partitioning.go @@ -0,0 +1,19 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package partitioning implements common partitioning functions. +package partitioning + +import "strconv" + +// DevName returns the devname for the partition on a disk. +func DevName(device string, part uint) string { + result := device + + if len(result) > 0 && result[len(result)-1] >= '0' && result[len(result)-1] <= '9' { + result += "p" + } + + return result + strconv.FormatUint(uint64(part), 10) +}