From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id CC9231FF13C for ; Thu, 30 Apr 2026 17:07:23 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A6640EFB1; Thu, 30 Apr 2026 17:07:23 +0200 (CEST) From: Robert Obkircher To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup 09/10] api2: backup: check space for fixed and dynamic index files Date: Thu, 30 Apr 2026 17:05:50 +0200 Message-ID: <20260430150607.330413-13-r.obkircher@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260430150607.330413-1-r.obkircher@proxmox.com> References: <20260430150607.330413-1-r.obkircher@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777561509940 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.059 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 5WPKKFX7KA5RQTNY5APUXPE63W6NFI32 X-Message-ID-Hash: 5WPKKFX7KA5RQTNY5APUXPE63W6NFI32 X-MailFrom: r.obkircher@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The dynamic index writer uses a 1 MiB buffer, so size checks only need to include that. The fixed index writer uses mmap+ftruncate, which makes it difficult to tell whether file system space has already been reserved. Because running out of space would risk getting killed with SIGBUS it is better to always check for the total size. On non-CoW file systems the risk could be reduced further by switching to fallocate. Signed-off-by: Robert Obkircher --- src/api2/backup/environment.rs | 23 +++++++++++++++++++++++ src/api2/backup/mod.rs | 19 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/api2/backup/environment.rs b/src/api2/backup/environment.rs index ab623f1ff..c297d78c5 100644 --- a/src/api2/backup/environment.rs +++ b/src/api2/backup/environment.rs @@ -377,6 +377,24 @@ impl BackupEnvironment { Ok(uid) } + pub fn fixed_writer_check_space(&self, wid: usize, offset: u64) -> Result<(), Error> { + let mut state = self.state.lock().unwrap(); + + state.ensure_unfinished()?; + + let data = match state.fixed_writers.get_mut(&wid) { + Some(data) => data, + None => bail!("fixed writer '{}' not registered", wid), + }; + + let content_size = data + .size + .unwrap_or_else(|| data.index.size().max(offset + data.chunk_size as u64)); + + self.datastore + .check_space(4096 + 32 * content_size.div_ceil(data.chunk_size as u64)) + } + /// Append chunk to dynamic writer pub fn dynamic_writer_append_chunk( &self, @@ -533,6 +551,8 @@ impl BackupEnvironment { ); } + self.datastore.check_space(1024 * 1024)?; + let expected_csum = data.index.close()?; data.closed = true; @@ -642,6 +662,9 @@ impl BackupEnvironment { } } + self.datastore + .check_space(4096 + data.index.index_length() as u64 * 32)?; + let expected_csum = data.index.close()?; data.closed = true; diff --git a/src/api2/backup/mod.rs b/src/api2/backup/mod.rs index 86ec49487..0edaca601 100644 --- a/src/api2/backup/mod.rs +++ b/src/api2/backup/mod.rs @@ -437,6 +437,8 @@ fn create_dynamic_index( bail!("wrong archive extension: '{}'", archive_name); } + env.datastore.check_space(1024 * 1024)?; + let mut path = env.backup_dir.relative_path(); path.push(archive_name); @@ -489,6 +491,8 @@ fn create_fixed_index( bail!("wrong archive extension: '{}'", archive_name); } + env.datastore.check_space(size.unwrap_or(4096 + 4096))?; + let mut path = env.backup_dir.relative_path(); path.push(&archive_name); @@ -610,6 +614,10 @@ fn dynamic_append( env.debug(format!("dynamic_append {} chunks", digest_list.len())); + // BufWriter capacity + new data + env.datastore + .check_space(1024 * 1024 + digest_list.len() as u64 * 40)?; + for (i, item) in digest_list.iter().enumerate() { let digest_str = item.as_str().unwrap(); let digest = <[u8; 32]>::from_hex(digest_str)?; @@ -683,10 +691,19 @@ fn fixed_append( env.debug(format!("fixed_append {} chunks", digest_list.len())); + let offset_list = offset_list + .iter() + .map(|o| o.as_u64().unwrap()) + .collect::>(); + + if let Some(max_offset) = offset_list.iter().max() { + env.fixed_writer_check_space(wid, *max_offset)?; + } + for (i, item) in digest_list.iter().enumerate() { let digest_str = item.as_str().unwrap(); let digest = <[u8; 32]>::from_hex(digest_str)?; - let offset = offset_list[i].as_u64().unwrap(); + let offset = offset_list[i]; let size = env .lookup_chunk(&digest) .ok_or_else(|| format_err!("no such chunk {}", digest_str))?; -- 2.47.3