]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared: Ensure COPY_HOLES copies trailing holes
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 26 Jan 2022 12:08:50 +0000 (12:08 +0000)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Wed, 26 Jan 2022 16:15:22 +0000 (01:15 +0900)
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.

src/shared/copy.c
src/test/test-copy.c

index 457488efa2b6675b603e74539ad57ad60edd2ceb..a92cfc0f6151f29e1dffdcacaf630effe8b04ecb 100644 (file)
@@ -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. */
 
index 5a1cf605c1ffa882c23b6240a0bda03c50ea4a90..6c7b8d220bc43cdb5b74ae740c4c385662097fa8 100644 (file)
@@ -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);