diff --git a/man/ostree.repo-config.xml b/man/ostree.repo-config.xml
index d187f89f..60458dfa 100644
--- a/man/ostree.repo-config.xml
+++ b/man/ostree.repo-config.xml
@@ -190,6 +190,14 @@ Boston, MA 02111-1307, USA.
unconfigured-state
If set, pulls from this remote will fail with the configured text. This is intended for OS vendors which have a subscription process to access content.
+
+
+ min-free-space-percent
+ Integer percentage value (0-99) that specifies a minimum
+ percentage of total space (in blocks) in the underlying filesystem to
+ keep free. The default value is 3.
+
+
diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c
index 9cc028e0..185ab82f 100644
--- a/src/libostree/ostree-repo-commit.c
+++ b/src/libostree/ostree-repo-commit.c
@@ -23,6 +23,7 @@
#include "config.h"
#include
+#include
#include
#include
#include
@@ -574,6 +575,24 @@ write_content_object (OstreeRepo *self,
else
size = 0;
+ /* Free space check; only applies during transactions */
+ if (self->min_free_space_percent > 0 && self->in_transaction)
+ {
+ g_mutex_lock (&self->txn_stats_lock);
+ g_assert_cmpint (self->txn_blocksize, >, 0);
+ const fsblkcnt_t object_blocks = (size / self->txn_blocksize) + 1;
+ if (object_blocks > self->max_txn_blocks)
+ {
+ g_mutex_unlock (&self->txn_stats_lock);
+ g_autofree char *formatted_required = g_format_size ((guint64)object_blocks * self->txn_blocksize);
+ return glnx_throw (error, "min-free-space-percent '%u%%' would be exceeded, %s more required",
+ self->min_free_space_percent, formatted_required);
+ }
+ /* This is the main bit that needs mutex protection */
+ self->max_txn_blocks -= object_blocks;
+ g_mutex_unlock (&self->txn_stats_lock);
+ }
+
/* For regular files, we create them with default mode, and only
* later apply any xattrs and setuid bits. The rationale here
* is that an attacker on the network with the ability to MITM
@@ -1080,6 +1099,29 @@ ostree_repo_prepare_transaction (OstreeRepo *self,
memset (&self->txn_stats, 0, sizeof (OstreeRepoTransactionStats));
self->in_transaction = TRUE;
+ if (self->min_free_space_percent > 0)
+ {
+ struct statvfs stvfsbuf;
+ if (TEMP_FAILURE_RETRY (fstatvfs (self->repo_dir_fd, &stvfsbuf)) < 0)
+ return glnx_throw_errno_prefix (error, "fstatvfs");
+ g_mutex_lock (&self->txn_stats_lock);
+ self->txn_blocksize = stvfsbuf.f_bsize;
+ /* Convert fragment to blocks to compute the total */
+ guint64 total_blocks = (stvfsbuf.f_frsize * stvfsbuf.f_blocks) / stvfsbuf.f_bsize;
+ /* Use the appropriate free block count if we're unprivileged */
+ guint64 bfree = (getuid () != 0 ? stvfsbuf.f_bavail : stvfsbuf.f_bfree);
+ guint64 reserved_blocks = ((double)total_blocks) * (self->min_free_space_percent/100.0);
+ if (bfree > reserved_blocks)
+ self->max_txn_blocks = bfree - reserved_blocks;
+ else
+ {
+ g_mutex_unlock (&self->txn_stats_lock);
+ g_autofree char *formatted_free = g_format_size (bfree * self->txn_blocksize);
+ return glnx_throw (error, "min-free-space-percent '%u%%' would be exceeded, %s available",
+ self->min_free_space_percent, formatted_free);
+ }
+ g_mutex_unlock (&self->txn_stats_lock);
+ }
gboolean ret_transaction_resume = FALSE;
if (!_ostree_repo_allocate_tmpdir (self->tmp_dir_fd,
diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h
index d518e52b..9e00cf40 100644
--- a/src/libostree/ostree-repo-private.h
+++ b/src/libostree/ostree-repo-private.h
@@ -20,6 +20,7 @@
#pragma once
+#include
#include "ostree-ref.h"
#include "ostree-repo.h"
#include "ostree-remote-private.h"
@@ -102,6 +103,9 @@ struct OstreeRepo {
GHashTable *txn_collection_refs; /* (element-type OstreeCollectionRef utf8) */
GMutex txn_stats_lock;
OstreeRepoTransactionStats txn_stats;
+ /* Implementation of min-free-space-percent */
+ gulong txn_blocksize;
+ fsblkcnt_t max_txn_blocks;
GMutex cache_lock;
guint dirmeta_cache_refcount;
@@ -123,6 +127,7 @@ struct OstreeRepo {
uid_t owner_uid;
uid_t target_owner_uid;
gid_t target_owner_gid;
+ guint min_free_space_percent;
guint test_error_flags; /* OstreeRepoTestErrorFlags */
diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c
index a02214e1..e29b8fca 100644
--- a/src/libostree/ostree-repo.c
+++ b/src/libostree/ostree-repo.c
@@ -44,6 +44,7 @@
#include
#include
#include
+#include
/**
* SECTION:ostree-repo
@@ -1964,6 +1965,20 @@ reload_core_config (OstreeRepo *self,
self->zlib_compression_level = OSTREE_ARCHIVE_DEFAULT_COMPRESSION_LEVEL;
}
+ { g_autofree char *min_free_space_percent_str = NULL;
+ /* If changing this, be sure to change the man page too */
+ const char *default_min_free_space = "3";
+
+ if (!ot_keyfile_get_value_with_default (self->config, "core", "min-free-space-percent",
+ default_min_free_space,
+ &min_free_space_percent_str, error))
+ return FALSE;
+
+ self->min_free_space_percent = g_ascii_strtoull (min_free_space_percent_str, NULL, 10);
+ if (self->min_free_space_percent > 99)
+ return glnx_throw (error, "Invalid min-free-space-percent '%s'", min_free_space_percent_str);
+ }
+
{
g_clear_pointer (&self->collection_id, g_free);
if (!ot_keyfile_get_value_with_default (self->config, "core", "collection-id",
diff --git a/tests/installed/itest-pull-space.sh b/tests/installed/itest-pull-space.sh
new file mode 100755
index 00000000..36703a40
--- /dev/null
+++ b/tests/installed/itest-pull-space.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# Test min-free-space-percent using loopback devices
+
+set -xeuo pipefail
+
+dn=$(dirname $0)
+. ${dn}/libinsttest.sh
+
+test_tmpdir=$(prepare_tmpdir)
+trap _tmpdir_cleanup EXIT
+
+cd ${test_tmpdir}
+truncate -s 100MB testblk.img
+blkdev=$(losetup --find --show $(pwd)/testblk.img)
+mkfs.xfs ${blkdev}
+mkdir mnt
+mount ${blkdev} mnt
+ostree --repo=mnt/repo init --mode=bare-user
+if ostree --repo=mnt/repo pull-local /ostree/repo ${host_commit} 2>err.txt; then
+ fatal "succeeded in doing a pull with no free space"
+fi
+assert_file_has_content err.txt "min-free-space-percent"
+umount mnt
+losetup -d ${blkdev}