blockdev-util: add helper for zeroing out a range of a block device

blockdev_zero_out() overwrites a byte range of a block device (or a regular
file, e.g. a disk image) with zeroes. On block devices we try to let 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 incomplete
sectors at the beginning and the end of the range are filled with explicit
zeroes, and only the aligned middle part is handed to the ioctl. If that
fails, explicit zeroes are written for that part too. On regular files a
hole is punched instead (which reads back as zeroes and keeps the file
sparse), falling back to explicit zeroes if the file system doesn't support
that.

(cherry picked from commit eb98b03118f27f33d3834ef8322f38391eb2c238)
This commit is contained in:
Lennart Poettering
2026-10-16 17:24:51 +01:00
committed by Luca Boccassi
parent 9752a9ef8f
commit c4e7a0fdfe
3 changed files with 208 additions and 0 deletions
+125
View File
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <linux/blkpg.h>
#include <linux/fs.h>
#include <sys/file.h>
@@ -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;
+1
View File
@@ -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);
+82
View File
@@ -1,12 +1,18 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <unistd.h>
#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);