diff --git a/src/shared/blockdev-util.c b/src/shared/blockdev-util.c index 7ddcb93daf..1cccbd52a1 100644 --- a/src/shared/blockdev-util.c +++ b/src/shared/blockdev-util.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include #include @@ -27,6 +28,9 @@ #include "path-util.h" #include "string-util.h" +/* Chunk size to use when writing zeroes manually, see blockdev_zero_out() */ +#define ZERO_OUT_BUFFER_SIZE (64U*U64_KB) + static int fd_get_devnum(int fd, BlockDeviceLookupFlags flags, dev_t *ret) { struct stat st; dev_t devnum; @@ -854,6 +858,127 @@ int blockdev_get_device_size(int fd, uint64_t *ret) { return 0; } +static int write_zeroes(int fd, uint64_t offset, uint64_t size) { + _cleanup_free_ uint8_t *buffer = NULL; + size_t bufsz; + + assert(fd >= 0); + + if (size == 0) + return 0; + + bufsz = MIN(size, ZERO_OUT_BUFFER_SIZE); + buffer = malloc0(bufsz); + if (!buffer) + return -ENOMEM; + + while (size > 0) { + ssize_t n; + + n = pwrite(fd, buffer, MIN(size, (uint64_t) bufsz), offset); + if (n < 0) { + if (errno == EINTR) + continue; + + return -errno; + } + if (n == 0) + return -EIO; + + assert((uint64_t) n <= size); + offset += n; + size -= n; + } + + return 0; +} + +int blockdev_zero_out(int fd, uint64_t offset, uint64_t size) { + struct stat st; + uint32_t ssz; + int r; + + assert(fd >= 0); + + /* Overwrites the specified range of a block device (or regular file) with zeroes. On block devices + * we try to make the kernel do the work via BLKZEROOUT, which it might be able to offload to the + * device. That ioctl operates on whole sectors only however, hence any incomplete sectors at the + * beginning and the end of the range are filled with explicit zeroes, and the aligned middle part is + * then handed to the ioctl. If that fails explicit zeroes are written for it too. On regular files + * (e.g. disk images) we punch a hole instead, which reads back as zeroes and keeps the file sparse; + * if the file system doesn't support that explicit zeroes are written as well. + * + * Note that the explicit writes to incomplete sectors rely on buffered I/O: the kernel then takes + * care of the read-modify-write of the affected sectors via the page cache. Hence this must not be + * used on fds opened with O_DIRECT. */ + + if (offset > UINT64_MAX - size) + return -EOVERFLOW; + + if (fstat(fd, &st) < 0) + return -errno; + + if (!IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFBLK)) + return -ENOTBLK; + + if (size == 0) /* check the size *after* validating the fd via fstat() */ + return 0; + + uint64_t end = offset + size; + + if (S_ISREG(st.st_mode)) { + uint64_t file_size = st.st_size; + + /* Punch a hole into the part of the range that is within the file. FALLOC_FL_KEEP_SIZE + * means the file is not grown, hence the part beyond the end of the file is handled + * separately below, by extending the file, which implicitly reads back as zeroes too. */ + if (offset < file_size && + fallocate(fd, FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE, offset, MIN(size, file_size - offset)) < 0) { + if (!ERRNO_IS_NOT_SUPPORTED(errno)) + return -errno; + + log_debug_errno(errno, "Failed to punch hole into regular file, writing zeroes manually: %m"); + return write_zeroes(fd, offset, size); + } + + if (end > file_size && ftruncate(fd, end) < 0) + return -errno; + + return 0; + } + + r = blockdev_get_sector_size(fd, &ssz); + if (r < 0) + return r; + + uint64_t aligned_start = ROUND_UP(offset, ssz), + aligned_end = ALIGN_DOWN_U64(end, ssz); + + if (aligned_start >= aligned_end) /* Range doesn't cover a single full sector? Then write it all manually */ + return write_zeroes(fd, offset, size); + + /* Fill up the incomplete sector at the beginning */ + r = write_zeroes(fd, offset, aligned_start - offset); + if (r < 0) + return r; + + /* Now let the kernel zero out the aligned part in the middle, if it can */ + if (ioctl(fd, BLKZEROOUT, (uint64_t[2]) { aligned_start, aligned_end - aligned_start }) < 0) { + + if (!ERRNO_IS_NOT_SUPPORTED(errno)) + return log_debug_errno(errno, "Failed to zero out block device via BLKZEROOUT: %m"); + + log_debug_errno(errno, "Failed to zero out block device via BLKZEROOUT, writing zeroes manually: %m"); + + r = write_zeroes(fd, aligned_start, aligned_end - aligned_start); + if (r < 0) + return r; + } + + /* And finally fill the incomplete sector at the end */ + return write_zeroes(fd, aligned_end, end - aligned_end); +} + int blockdev_get_root(int level, dev_t *ret) { _cleanup_free_ char *p = NULL; dev_t devno; diff --git a/src/shared/blockdev-util.h b/src/shared/blockdev-util.h index 48962f8b26..424594a9ef 100644 --- a/src/shared/blockdev-util.h +++ b/src/shared/blockdev-util.h @@ -53,6 +53,7 @@ int block_device_remove_all_partitions(sd_device *dev, int fd); int blockdev_get_sector_size(int fd, uint32_t *ret); int blockdev_get_device_size(int fd, uint64_t *ret); +int blockdev_zero_out(int fd, uint64_t offset, uint64_t size); int blockdev_get_root(int level, dev_t *ret); diff --git a/src/test/test-blockdev-util.c b/src/test/test-blockdev-util.c index abef98b507..08edcdb537 100644 --- a/src/test/test-blockdev-util.c +++ b/src/test/test-blockdev-util.c @@ -1,12 +1,18 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#include + #include "sd-daemon.h" +#include "alloc-util.h" #include "blockdev-util.h" #include "device-util.h" #include "errno-util.h" #include "fd-util.h" +#include "io-util.h" #include "tests.h" +#include "tmpfile-util.h" static void test_path_is_encrypted_one(const char *p, int expect) { int r; @@ -103,4 +109,80 @@ TEST(partition_node_of) { test_partition_node_of_one(".", 1, NULL, -EADDRNOTAVAIL); } +static void verify_range(int fd, uint64_t offset, uint64_t size, uint8_t expected) { + uint8_t buffer[4096]; + + /* Checks that every byte in the specified range of the file is 'expected' */ + + while (size > 0) { + ssize_t n; + + n = pread(fd, buffer, MIN(size, sizeof(buffer)), offset); + ASSERT_OK_ERRNO(n); + ASSERT_GT(n, 0); + + for (ssize_t i = 0; i < n; i++) + ASSERT_EQ(buffer[i], expected); + + offset += n; + size -= n; + } +} + +TEST(blockdev_zero_out) { + _cleanup_(unlink_tempfilep) char path[] = "/tmp/test-blockdev-zero-out.XXXXXX"; + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ uint8_t *pattern = NULL; + struct stat st; + + /* Larger than the 64K buffer write_zeroes() operates with internally, so that the chunking is exercised */ + const uint64_t file_size = 200U*1024U; + + fd = mkostemp_safe(path); + ASSERT_OK(fd); + + ASSERT_NOT_NULL(pattern = malloc(file_size)); + memset(pattern, 0xAA, file_size); + ASSERT_OK(loop_write(fd, pattern, file_size)); + + /* Zero-sized range is a NOP */ + ASSERT_OK(blockdev_zero_out(fd, 17, 0)); + verify_range(fd, 0, file_size, 0xAA); + ASSERT_OK_ERRNO(fstat(fd, &st)); + ASSERT_EQ((uint64_t) st.st_size, file_size); + + /* Overflowing range is refused */ + ASSERT_ERROR(blockdev_zero_out(fd, UINT64_MAX - 5, 10), EOVERFLOW); + ASSERT_ERROR(blockdev_zero_out(fd, UINT64_MAX, 1), EOVERFLOW); + verify_range(fd, 0, file_size, 0xAA); + + /* Unaligned range in the middle of the file, spanning multiple write_zeroes() chunks; bytes outside of + * it must stay intact */ + const uint64_t offset = 1234, size = 150U*1024U + 77; + ASSERT_OK(blockdev_zero_out(fd, offset, size)); + verify_range(fd, 0, offset, 0xAA); + verify_range(fd, offset, size, 0); + verify_range(fd, offset + size, file_size - offset - size, 0xAA); + ASSERT_OK_ERRNO(fstat(fd, &st)); + ASSERT_EQ((uint64_t) st.st_size, file_size); + + /* Range extending beyond the end of the file grows it */ + ASSERT_OK(blockdev_zero_out(fd, file_size - 100, 300)); + verify_range(fd, offset + size, file_size - offset - size - 100, 0xAA); + verify_range(fd, file_size - 100, 300, 0); + ASSERT_OK_ERRNO(fstat(fd, &st)); + ASSERT_EQ((uint64_t) st.st_size, file_size + 200); + + /* Range entirely beyond the end of the file grows it too, the gap reads back as zeroes */ + ASSERT_OK(blockdev_zero_out(fd, file_size + 1000, 50)); + verify_range(fd, file_size - 100, 1150, 0); + ASSERT_OK_ERRNO(fstat(fd, &st)); + ASSERT_EQ((uint64_t) st.st_size, file_size + 1050); + + /* Neither a regular file nor a block device */ + _cleanup_close_pair_ int pipe_fds[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(pipe2(pipe_fds, O_CLOEXEC)); + ASSERT_ERROR(blockdev_zero_out(pipe_fds[1], 0, 10), ENOTBLK); +} + DEFINE_TEST_MAIN(LOG_INFO);