From: Daan De Meyer Date: Wed, 26 Jan 2022 12:08:50 +0000 (+0000) Subject: shared: Ensure COPY_HOLES copies trailing holes X-Git-Tag: v251-rc1~456 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f82f0b993740f4a9a3719c4df7c185411710df2b;p=thirdparty%2Fsystemd.git shared: Ensure COPY_HOLES copies trailing holes Previously, files with a hole at the end would get silently truncated which breaks reading journal files. This commit makes sure that holes are punched in existing space and if no more space is available, that we grow the file and the hole by using ftruncate(). The corresponding test is extended to put a hole at the end of the file and we make sure that hole is copied correctly. --- diff --git a/src/shared/copy.c b/src/shared/copy.c index 457488efa2b..a92cfc0f615 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -107,6 +107,45 @@ static int look_for_signals(CopyFlags copy_flags) { return 0; } +static int create_hole(int fd, off_t size) { + off_t offset; + off_t end; + + offset = lseek(fd, 0, SEEK_CUR); + if (offset < 0) + return -errno; + + end = lseek(fd, 0, SEEK_END); + if (end < 0) + return -errno; + + /* If we're not at the end of the target file, punch a hole in the existing space using fallocate(). */ + + if (offset < end && fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, MIN(size, end - offset)) < 0) + return -errno; + + if (end - offset >= size) { + /* If we've created the full hole, set the file pointer to the end of the hole we created and exit. */ + if (lseek(fd, offset + size, SEEK_SET) < 0) + return -errno; + + return 0; + } + + /* If we haven't created the full hole, use ftruncate() to grow the file (and the hole) to the + * required size and move the file pointer to the end of the file. */ + + size -= end - offset; + + if (ftruncate(fd, end + size) < 0) + return -errno; + + if (lseek(fd, 0, SEEK_END) < 0) + return -errno; + + return 0; +} + int copy_bytes_full( int fdf, int fdt, uint64_t max_bytes, @@ -220,8 +259,11 @@ int copy_bytes_full( /* If we're in a hole (current offset is not a data offset), create a hole of the * same size in the target file. */ - if (e > c && lseek(fdt, e - c, SEEK_CUR) < 0) - return -errno; + if (e > c) { + r = create_hole(fdt, e - c); + if (r < 0) + return r; + } c = e; /* Set c to the start of the data segment. */ diff --git a/src/test/test-copy.c b/src/test/test-copy.c index 5a1cf605c1f..6c7b8d220bc 100644 --- a/src/test/test-copy.c +++ b/src/test/test-copy.c @@ -327,6 +327,7 @@ TEST_RET(copy_holes) { char fn[] = "/var/tmp/test-copy-hole-fd-XXXXXX"; char fn_copy[] = "/var/tmp/test-copy-hole-fd-XXXXXX"; struct stat stat; + off_t blksz; int r, fd, fd_copy; fd = mkostemp_safe(fn); @@ -340,11 +341,18 @@ TEST_RET(copy_holes) { return log_tests_skipped("Filesystem doesn't support hole punching"); assert_se(r >= 0); - /* We need to make sure to create a large enough hole and to write some data after it, otherwise - * filesystems (btrfs) might silently discard it. */ + assert_se(fstat(fd, &stat) >= 0); + blksz = stat.st_blksize; + char buf[blksz]; - assert_se(lseek(fd, 1024 * 1024, SEEK_CUR) >= 0); - assert_se(write(fd, "abc", strlen("abc")) >= 0); + /* We need to make sure to create hole in multiples of the block size, otherwise filesystems (btrfs) + * might silently truncate/extend the holes. */ + + assert_se(lseek(fd, blksz, SEEK_CUR) >= 0); + assert_se(write(fd, buf, blksz) >= 0); + assert_se(lseek(fd, 0, SEEK_END) == 2 * blksz); + /* Only ftruncate() can create holes at the end of a file. */ + assert_se(ftruncate(fd, 3 * blksz) >= 0); assert_se(lseek(fd, 0, SEEK_SET) >= 0); assert_se(copy_bytes(fd, fd_copy, UINT64_MAX, COPY_HOLES) >= 0); @@ -352,11 +360,13 @@ TEST_RET(copy_holes) { /* Test that the hole starts at the beginning of the file. */ assert_se(lseek(fd_copy, 0, SEEK_HOLE) == 0); /* Test that the hole has the expected size. */ - assert_se(lseek(fd_copy, 0, SEEK_DATA) == 1024 * 1024); + assert_se(lseek(fd_copy, 0, SEEK_DATA) == blksz); + assert_se(lseek(fd_copy, blksz, SEEK_HOLE) == 2 * blksz); + assert_se(lseek(fd_copy, 2 * blksz, SEEK_DATA) < 0 && errno == ENXIO); /* Test that the copied file has the correct size. */ assert_se(fstat(fd_copy, &stat) >= 0); - assert_se(stat.st_size == 1024 * 1024 + strlen("abc")); + assert_se(stat.st_size == 3 * blksz); close(fd); close(fd_copy);