diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index 9c4f3770346..57b41d26b83 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -1960,7 +1960,6 @@ static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t * static int home_truncate( UserRecord *h, int fd, - const char *path, uint64_t size) { bool trunc; @@ -1968,7 +1967,6 @@ static int home_truncate( assert(h); assert(fd >= 0); - assert(path); trunc = user_record_luks_discard(h); if (!trunc) { @@ -1986,14 +1984,14 @@ static int home_truncate( if (r < 0) { if (ERRNO_IS_DISK_SPACE(errno)) { - log_error_errno(errno, "Not enough disk space to allocate home."); + log_debug_errno(errno, "Not enough disk space to allocate home of size %s.", FORMAT_BYTES(size)); return -ENOSPC; /* make recognizable */ } - return log_error_errno(errno, "Failed to truncate home image %s: %m", path); + return log_error_errno(errno, "Failed to truncate home image: %m"); } - return 0; + return !trunc; /* Return == 0 if we managed to truncate, > 0 if we managed to allocate */ } int home_create_luks( @@ -2175,7 +2173,7 @@ int home_create_luks( log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r, "Failed to set file attributes on %s, ignoring: %m", setup->temporary_image_path); - r = home_truncate(h, setup->image_fd, setup->temporary_image_path, host_size); + r = home_truncate(h, setup->image_fd, host_size); if (r < 0) return r; @@ -2764,6 +2762,39 @@ static int get_smallest_fs_size(int fd, uint64_t *ret) { return 0; } +static int get_largest_image_size(int fd, const struct stat *st, uint64_t *ret) { + uint64_t used, avail, sum; + struct statfs sfs; + int r; + + assert(fd >= 0); + assert(st); + assert(ret); + + /* Determines the maximum file size we might be able to grow the image file referenced by the fd to. */ + + r = stat_verify_regular(st); + if (r < 0) + return log_error_errno(r, "Image file is not a regular file, refusing: %m"); + + if (syncfs(fd) < 0) + return log_error_errno(errno, "Failed to synchronize file system backing image file: %m"); + + if (fstatfs(fd, &sfs) < 0) + return log_error_errno(errno, "Failed to statfs() image file: %m"); + + used = (uint64_t) st->st_blocks * 512; + avail = (uint64_t) sfs.f_bsize * sfs.f_bavail; + + if (avail > UINT64_MAX - used) + sum = UINT64_MAX; + else + sum = avail + used; + + *ret = DISK_SIZE_ROUND_DOWN(MIN(sum, USER_DISK_SIZE_MAX)); + return 0; +} + static int resize_fs_loop( UserRecord *h, HomeSetup *setup, @@ -2851,6 +2882,77 @@ static int resize_fs_loop( return 0; } +static int resize_image_loop( + UserRecord *h, + HomeSetup *setup, + uint64_t old_image_size, + uint64_t new_image_size, + uint64_t *ret_image_size) { + + uint64_t current_image_size; + unsigned n_iterations = 0; + int r; + + assert(h); + assert(setup); + assert(setup->image_fd >= 0); + + /* A bisection loop trying to find the closest size to what the user asked for. (Well, we bisect like + * this only when we *grow* the image — if we shrink the image then there's no need to bisect.) */ + + current_image_size = old_image_size; + for (uint64_t lower_boundary = old_image_size, upper_boundary = new_image_size, try_image_size = new_image_size;;) { + bool worked; + + n_iterations++; + + r = home_truncate(h, setup->image_fd, try_image_size); + if (r < 0) { + if (!ERRNO_IS_DISK_SPACE(r) || new_image_size < old_image_size) /* Not a disk space issue? Not trying to grow? */ + return r; + + log_debug_errno(r, "Growing from %s to %s didn't work, not enough space on backing disk.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size)); + worked = false; + } else if (r > 0) { /* Success: allocation worked */ + log_debug("Resizing from %s to %s via allocation worked successfully.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size)); + current_image_size = try_image_size; + worked = true; + } else { /* Success, but through truncation, not allocation. */ + log_debug("Resizing from %s to %s via truncation worked successfully.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(try_image_size)); + current_image_size = try_image_size; + break; /* there's no point in the bisection logic if this was plain truncation and + * not allocation, let's exit immediately. */ + } + + if (new_image_size < old_image_size) /* If we are shrinking we are done after one iteration */ + break; + + /* If we are growing then let's adjust our bisection boundaries and try again */ + if (worked) + lower_boundary = MAX(lower_boundary, try_image_size); + else + upper_boundary = MIN(upper_boundary, try_image_size); + + if (lower_boundary >= upper_boundary) { + log_debug("Image can't be grown further (range to try is empty)."); + break; + } + + try_image_size = DISK_SIZE_ROUND_DOWN(lower_boundary + (upper_boundary - lower_boundary) / 2); + if (try_image_size <= lower_boundary || try_image_size >= upper_boundary) { + log_debug("Image can't be grown further (remaining range to try too small)."); + break; + } + } + + log_debug("Bisection loop completed after %u iterations.", n_iterations); + + if (ret_image_size) + *ret_image_size = current_image_size; + + return 0; +} + int home_resize_luks( UserRecord *h, HomeSetupFlags flags, @@ -2988,11 +3090,17 @@ int home_resize_luks( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing."); if (S_ISREG(st.st_mode)) { - uint64_t partition_table_extra; + uint64_t partition_table_extra, largest_size; partition_table_extra = old_image_size - setup->partition_size; - if (new_image_size <= partition_table_extra) + r = get_largest_image_size(setup->image_fd, &st, &largest_size); + if (r < 0) + return r; + if (new_image_size > largest_size) + new_image_size = largest_size; + + if (new_image_size < partition_table_extra) new_image_size = partition_table_extra; new_partition_size = DISK_SIZE_ROUND_DOWN(new_image_size - partition_table_extra); @@ -3109,12 +3217,35 @@ int home_resize_luks( if (new_fs_size > old_fs_size) { /* → Grow */ if (S_ISREG(st.st_mode)) { + uint64_t resized_image_size; + /* Grow file size */ - r = home_truncate(h, image_fd, ip, new_image_size); + r = resize_image_loop(h, setup, old_image_size, new_image_size, &resized_image_size); if (r < 0) return r; - log_info("Growing of image file completed."); + if (resized_image_size == old_image_size) { + log_info("Couldn't change image size."); + return 0; + } + + assert(resized_image_size > old_image_size); + + log_info("Growing of image file from %s to %s completed.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(resized_image_size)); + + if (resized_image_size < new_image_size) { + uint64_t sub; + + /* If the growing we managed to do is smaller than what we wanted we need to + * adjust the partition/file system sizes we are going for, too */ + sub = new_image_size - resized_image_size; + assert(new_partition_size >= sub); + new_partition_size -= sub; + assert(new_fs_size >= sub); + new_fs_size -= sub; + } + + new_image_size = resized_image_size; } else { assert(S_ISBLK(st.st_mode)); assert(new_image_size == old_image_size);