osbase/blockdev: add tests, fix minor issues

Add a lot of bounds checks which should make BlockDev safer to use. Fix
a bug in the ReadWriteSeeker.Seek function with io.SeekEnd; the offset
should be added to, not subtracted from the size. Add the Sync()
function to the BlockDev interface.

Change-Id: I247095b3dbc6410064844b4ac7c6208d88a7abcd
Reviewed-on: https://review.monogon.dev/c/monogon/+/3338
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/blockdev/blockdev_linux.go b/osbase/blockdev/blockdev_linux.go
index c5fa784..f6d5b4c 100644
--- a/osbase/blockdev/blockdev_linux.go
+++ b/osbase/blockdev/blockdev_linux.go
@@ -5,6 +5,7 @@
 import (
 	"errors"
 	"fmt"
+	"io"
 	"math/bits"
 	"os"
 	"syscall"
@@ -21,10 +22,32 @@
 }
 
 func (d *Device) ReadAt(p []byte, off int64) (n int, err error) {
+	size := d.blockSize * d.blockCount
+	if off > size {
+		return 0, io.EOF
+	}
+	if int64(len(p)) > size-off {
+		n, err = d.backend.ReadAt(p[:size-off], off)
+		if err == nil {
+			err = io.EOF
+		}
+		return
+	}
 	return d.backend.ReadAt(p, off)
 }
 
 func (d *Device) WriteAt(p []byte, off int64) (n int, err error) {
+	size := d.blockSize * d.blockCount
+	if off > size {
+		return 0, ErrOutOfBounds
+	}
+	if int64(len(p)) > size-off {
+		n, err = d.backend.WriteAt(p[:size-off], off)
+		if err == nil {
+			err = ErrOutOfBounds
+		}
+		return
+	}
 	return d.backend.WriteAt(p, off)
 }
 
@@ -40,7 +63,17 @@
 	return d.blockSize
 }
 
+func (d *Device) OptimalBlockSize() int64 {
+	return d.blockSize
+}
+
 func (d *Device) Discard(startByte int64, endByte int64) error {
+	if err := validAlignedRange(d, startByte, endByte); err != nil {
+		return err
+	}
+	if startByte == endByte {
+		return nil
+	}
 	var args [2]uint64
 	var err unix.Errno
 	args[0] = uint64(startByte)
@@ -59,11 +92,13 @@
 	return nil
 }
 
-func (d *Device) OptimalBlockSize() int64 {
-	return d.blockSize
-}
-
 func (d *Device) Zero(startByte int64, endByte int64) error {
+	if err := validAlignedRange(d, startByte, endByte); err != nil {
+		return err
+	}
+	if startByte == endByte {
+		return nil
+	}
 	var args [2]uint64
 	var err error
 	args[0] = uint64(startByte)
@@ -92,6 +127,10 @@
 	return nil
 }
 
+func (d *Device) Sync() error {
+	return d.backend.Sync()
+}
+
 // RefreshPartitionTable refreshes the kernel's view of the partition table
 // after changes made from userspace.
 func (d *Device) RefreshPartitionTable() error {
@@ -165,7 +204,7 @@
 
 func CreateFile(name string, blockSize int64, blockCount int64) (*File, error) {
 	if blockSize < 512 {
-		return nil, fmt.Errorf("blockSize must be bigger than 512 bytes")
+		return nil, fmt.Errorf("blockSize must be at least 512 bytes")
 	}
 	if bits.OnesCount64(uint64(blockSize)) != 1 {
 		return nil, fmt.Errorf("blockSize must be a power of two")
@@ -187,10 +226,32 @@
 }
 
 func (d *File) ReadAt(p []byte, off int64) (n int, err error) {
+	size := d.blockSize * d.blockCount
+	if off > size {
+		return 0, io.EOF
+	}
+	if int64(len(p)) > size-off {
+		n, err = d.backend.ReadAt(p[:size-off], off)
+		if err == nil {
+			err = io.EOF
+		}
+		return
+	}
 	return d.backend.ReadAt(p, off)
 }
 
 func (d *File) WriteAt(p []byte, off int64) (n int, err error) {
+	size := d.blockSize * d.blockCount
+	if off > size {
+		return 0, ErrOutOfBounds
+	}
+	if int64(len(p)) > size-off {
+		n, err = d.backend.WriteAt(p[:size-off], off)
+		if err == nil {
+			err = ErrOutOfBounds
+		}
+		return
+	}
 	return d.backend.WriteAt(p, off)
 }
 
@@ -206,7 +267,17 @@
 	return d.blockSize
 }
 
+func (d *File) OptimalBlockSize() int64 {
+	return d.blockSize
+}
+
 func (d *File) Discard(startByte int64, endByte int64) error {
+	if err := validAlignedRange(d, startByte, endByte); err != nil {
+		return err
+	}
+	if startByte == endByte {
+		return nil
+	}
 	var err error
 	if ctrlErr := d.rawConn.Control(func(fd uintptr) {
 		// There is FALLOC_FL_NO_HIDE_STALE, but it's not implemented by
@@ -224,11 +295,13 @@
 	return nil
 }
 
-func (d *File) OptimalBlockSize() int64 {
-	return d.blockSize
-}
-
 func (d *File) Zero(startByte int64, endByte int64) error {
+	if err := validAlignedRange(d, startByte, endByte); err != nil {
+		return err
+	}
+	if startByte == endByte {
+		return nil
+	}
 	var err error
 	if ctrlErr := d.rawConn.Control(func(fd uintptr) {
 		// Tell the filesystem to punch out the given blocks.
@@ -246,3 +319,7 @@
 	}
 	return nil
 }
+
+func (d *File) Sync() error {
+	return d.backend.Sync()
+}