diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index b87866ff34..425abe8bd6 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -2441,6 +2441,30 @@ get_kernel_layout_size (OstreeSysroot *self, OstreeDeployment *deployment, guint return TRUE; } +/* This is a roundabout but more trustworthy way of doing a space check than + * relying on statvfs's f_bfree when you know the size of the objects. */ +static gboolean +dfd_fallocate_check (int dfd, __off_t len, gboolean *out_passed, GError **error) +{ + g_auto (GLnxTmpfile) tmpf = { + 0, + }; + if (!glnx_open_tmpfile_linkable_at (dfd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error)) + return FALSE; + + *out_passed = TRUE; + /* There's glnx_try_fallocate, but not with the same error semantics. */ + if (TEMP_FAILURE_RETRY (fallocate (tmpf.fd, 0, 0, len)) < 0) + { + if (G_IN_SET (errno, ENOSYS, EOPNOTSUPP)) + return TRUE; + else if (errno != ENOSPC) + return glnx_throw_errno_prefix (error, "fallocate"); + *out_passed = FALSE; + } + return TRUE; +} + /* Analyze /boot and figure out if the new deployments won't fit in the * remaining space. If they won't, check if deleting the deployments that are * getting rotated out (e.g. the current rollback) would free up sufficient @@ -2534,7 +2558,7 @@ auto_early_prune_old_deployments (OstreeSysroot *self, GPtrArray *new_deployment continue; } - guint64 bootdir_size; + guint64 bootdir_size = 0; if (!get_kernel_layout_size (self, deployment, &bootdir_size, cancellable, error)) return FALSE; @@ -2553,32 +2577,45 @@ auto_early_prune_old_deployments (OstreeSysroot *self, GPtrArray *new_deployment net_new_bootcsum_dirs_total_size += bootdir_size; } - /* get bootfs free space */ - struct statvfs stvfsbuf; - if (TEMP_FAILURE_RETRY (fstatvfs (self->boot_fd, &stvfsbuf)) < 0) - return glnx_throw_errno_prefix (error, "fstatvfs(boot)"); - - guint64 available_size = stvfsbuf.f_bsize * stvfsbuf.f_bfree; - - /* does the bootfs have enough free space for net-new bootdirs? */ - if (net_new_bootcsum_dirs_total_size <= available_size) - return TRUE; /* nothing to do! */ + { + gboolean bootfs_has_space = FALSE; + if (!dfd_fallocate_check (self->boot_fd, net_new_bootcsum_dirs_total_size, &bootfs_has_space, + error)) + return glnx_prefix_error (error, "Checking if bootfs has space"); + + /* does the bootfs have enough free space for temporarily holding both the new + * and old bootdirs? */ + if (bootfs_has_space) + return TRUE; /* nothing to do! */ + } /* OK, we would fail if we tried to write the new bootdirs. Is it salvageable? * First, calculate how much space we could save with the bootcsums scheduled * for removal. */ - guint64 size_to_remove = 0; + guint64 bootcsum_dirs_to_remove_total_size = 0; GLNX_HASH_TABLE_FOREACH_KV (current_bootcsums, const char *, bootcsum, gpointer, sizep) { if (!g_hash_table_contains (new_bootcsums, bootcsum)) - size_to_remove += GPOINTER_TO_UINT (sizep); + bootcsum_dirs_to_remove_total_size += GPOINTER_TO_UINT (sizep); } - if (net_new_bootcsum_dirs_total_size > (available_size + size_to_remove)) + if (net_new_bootcsum_dirs_total_size > bootcsum_dirs_to_remove_total_size) { - /* Even if we auto-pruned, the new bootdirs wouldn't fit. Just let the - * code continue and let it hit ENOSPC. */ - return TRUE; + /* Check whether if we did early prune, we'd have enough space to write + * the new bootcsum dirs. */ + gboolean bootfs_has_space = FALSE; + if (!dfd_fallocate_check ( + self->boot_fd, net_new_bootcsum_dirs_total_size - bootcsum_dirs_to_remove_total_size, + &bootfs_has_space, error)) + return glnx_prefix_error (error, "Checking if bootfs has space"); + + if (!bootfs_has_space) + { + /* Even if we auto-pruned, the new bootdirs wouldn't fit. Just let the + * code continue and let it hit ENOSPC. */ + g_printerr ("Disabling auto-prune optimization; insufficient space left in bootfs\n"); + return TRUE; + } } g_printerr ("Insufficient space left in bootfs; updating bootloader in two steps\n"); @@ -2910,7 +2947,6 @@ lint_deployment_fs (OstreeSysroot *self, OstreeDeployment *deployment, int deplo g_auto (GLnxDirFdIterator) dfd_iter = { 0, }; - glnx_autofd int dest_dfd = -1; gboolean exists; if (!ot_dfd_iter_init_allow_noent (deployment_dfd, "var", &dfd_iter, &exists, error)) diff --git a/tests/kolainst/destructive/auto-prune.sh b/tests/kolainst/destructive/auto-prune.sh index 597da5a094..70b71868dd 100755 --- a/tests/kolainst/destructive/auto-prune.sh +++ b/tests/kolainst/destructive/auto-prune.sh @@ -9,9 +9,9 @@ set -xeuo pipefail cd /root mkdir -p rootfs/usr/lib/modules/`uname -r` cp /usr/lib/modules/`uname -r`/vmlinuz rootfs/usr/lib/modules/`uname -r` -echo 1 >> rootfs/usr/lib/modules/`uname -r`/vmlinuz +dd if=/dev/urandom of=rootfs/usr/lib/modules/`uname -r`/vmlinuz count=1 conv=notrunc status=none ostree commit --base "${host_refspec}" -P --tree=dir=rootfs -b modkernel1 -echo 1 >> rootfs/usr/lib/modules/`uname -r`/vmlinuz +dd if=/dev/urandom of=rootfs/usr/lib/modules/`uname -r`/vmlinuz count=1 conv=notrunc status=none ostree commit --base "${host_refspec}" -P --tree=dir=rootfs -b modkernel2 assert_bootfs_has_n_bootcsum_dirs() { @@ -25,8 +25,9 @@ assert_bootfs_has_n_bootcsum_dirs() { } consume_bootfs_space() { - local free_blocks=$(stat --file-system /boot -c '%a') - local block_size=$(stat --file-system /boot -c '%s') + local free_blocks block_size + free_blocks=${1:-$(stat --file-system /boot -c '%a')} + block_size=$(stat --file-system /boot -c '%s') # leave 1 block free unshare -m bash -c \ "mount -o rw,remount /boot && \ @@ -46,6 +47,7 @@ rpm-ostree rebase :modkernel1 if OSTREE_SYSROOT_OPTS=early-prune ostree admin finalize-staged |& tee out.txt; then assert_not_reached "successfully wrote to filled up bootfs" fi +assert_file_has_content out.txt "Disabling auto-prune optimization; insufficient space left in bootfs" assert_file_has_content out.txt "No space left on device" rm out.txt unconsume_bootfs_space @@ -91,4 +93,43 @@ rm out.txt assert_bootfs_has_n_bootcsum_dirs 2 assert_not_streq "$bootloader_orig" "$(sha256sum /boot/loader/entries/*)" +# This next test relies on the fact that FCOS currently uses ext4 for /boot. +# If that ever changes, we can reprovision boot to be ext4. +if [[ $(findmnt -no FSTYPE /boot) != ext4 ]]; then + assert_not_reached "/boot is not ext4" +fi + +# Put modkernel2 in rollback position +rpm-ostree rollback + +# Below, we test that a bootcsum dir sized below f_bfree but still large enough +# to not actually fit (because some filesystems like ext4 include reserved +# overhead in their f_bfree count for some reason) will still trigger the auto- +# prune logic. + +unconsume_bootfs_space + +# Size the bigfile just right so that the kernel+initrd will be just at the max +# limit according to f_bfree. +unshare -m bash -c \ + "mount -o rw,remount /boot && \ + cp /usr/lib/modules/`uname -r`/{vmlinuz,initramfs.img} /boot" +free_blocks=$(stat --file-system /boot -c '%f') +unshare -m bash -c \ + "mount -o rw,remount /boot && rm /boot/{vmlinuz,initramfs.img}" +consume_bootfs_space "$((free_blocks))" + +rpm-ostree rebase :modkernel1 +if ostree admin finalize-staged |& tee out.txt; then + assert_not_reached "successfully wrote kernel without auto-pruning" +fi +assert_file_has_content out.txt "No space left on device" +rm out.txt + +# now, try again but with auto-pruning enabled +rpm-ostree rebase :modkernel1 +OSTREE_SYSROOT_OPTS=early-prune ostree admin finalize-staged |& tee out.txt +assert_file_has_content out.txt "updating bootloader in two steps" +rm out.txt + echo "ok bootfs auto-prune"