* [PATCH proxmox-backup v2 1/5] datastore: data blob: refactor crypt mode method
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
@ 2026-05-07 13:01 ` Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method Christian Ebner
` (5 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Christian Ebner @ 2026-05-07 13:01 UTC (permalink / raw)
To: pbs-devel
Improve code style and readability by using a simple match statement
instead of chained if blocks.
No functional changes.
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
pbs-datastore/src/data_blob.rs | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/pbs-datastore/src/data_blob.rs b/pbs-datastore/src/data_blob.rs
index b434243c0..7761bbae7 100644
--- a/pbs-datastore/src/data_blob.rs
+++ b/pbs-datastore/src/data_blob.rs
@@ -167,17 +167,11 @@ impl DataBlob {
/// Get the encryption mode for this blob.
pub fn crypt_mode(&self) -> Result<CryptMode, Error> {
- let magic = self.magic();
-
- Ok(
- if magic == &UNCOMPRESSED_BLOB_MAGIC_1_0 || magic == &COMPRESSED_BLOB_MAGIC_1_0 {
- CryptMode::None
- } else if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0 {
- CryptMode::Encrypt
- } else {
- bail!("Invalid blob magic number.");
- },
- )
+ match *self.magic() {
+ UNCOMPRESSED_BLOB_MAGIC_1_0 | COMPRESSED_BLOB_MAGIC_1_0 => Ok(CryptMode::None),
+ ENCR_COMPR_BLOB_MAGIC_1_0 | ENCRYPTED_BLOB_MAGIC_1_0 => Ok(CryptMode::Encrypt),
+ _ => bail!("Invalid blob magic number."),
+ }
}
/// Decode blob data
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 1/5] datastore: data blob: refactor crypt mode method Christian Ebner
@ 2026-05-07 13:01 ` Christian Ebner
2026-07-22 11:31 ` Robert Obkircher
2026-05-07 13:01 ` [PATCH proxmox-backup v2 3/5] client: backup writer: pass no crypt config to manifest blob decoder Christian Ebner
` (4 subsequent siblings)
6 siblings, 1 reply; 11+ messages in thread
From: Christian Ebner @ 2026-05-07 13:01 UTC (permalink / raw)
To: pbs-devel
Improve code style and readability by using a single match statement
instead of chained if statements and deduplicate the common digest
verificaton, performed on the decoded blob data.
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
pbs-datastore/src/data_blob.rs | 86 +++++++++++++++++-----------------
1 file changed, 43 insertions(+), 43 deletions(-)
diff --git a/pbs-datastore/src/data_blob.rs b/pbs-datastore/src/data_blob.rs
index 7761bbae7..3c05a6106 100644
--- a/pbs-datastore/src/data_blob.rs
+++ b/pbs-datastore/src/data_blob.rs
@@ -180,57 +180,57 @@ impl DataBlob {
config: Option<&CryptConfig>,
digest: Option<&[u8; 32]>,
) -> Result<Vec<u8>, Error> {
- let magic = self.magic();
+ let magic = *self.magic();
- if magic == &UNCOMPRESSED_BLOB_MAGIC_1_0 {
- let data_start = std::mem::size_of::<DataBlobHeader>();
- let data = self.raw_data[data_start..].to_vec();
- if let Some(digest) = digest {
- Self::verify_digest(&data, None, digest)?;
+ let (data, crypt_config) = match magic {
+ UNCOMPRESSED_BLOB_MAGIC_1_0 => {
+ let data_start = std::mem::size_of::<DataBlobHeader>();
+ let data = self.raw_data[data_start..].to_vec();
+ (data, None)
}
- Ok(data)
- } else if magic == &COMPRESSED_BLOB_MAGIC_1_0 {
- let data_start = std::mem::size_of::<DataBlobHeader>();
- let mut reader = &self.raw_data[data_start..];
- let data = zstd::stream::decode_all(&mut reader)?;
- // zstd::block::decompress is about 10% slower
- // let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
- if let Some(digest) = digest {
- Self::verify_digest(&data, None, digest)?;
+ COMPRESSED_BLOB_MAGIC_1_0 => {
+ let data_start = std::mem::size_of::<DataBlobHeader>();
+ let mut reader = &self.raw_data[data_start..];
+ let data = zstd::stream::decode_all(&mut reader)?;
+ // zstd::block::decompress is about 10% slower
+ // let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
+ (data, None)
}
- Ok(data)
- } else if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0 {
- let header_len = std::mem::size_of::<EncryptedDataBlobHeader>();
- let head = unsafe {
- (&self.raw_data[..header_len]).read_le_value::<EncryptedDataBlobHeader>()?
- };
+ ENCR_COMPR_BLOB_MAGIC_1_0 | ENCRYPTED_BLOB_MAGIC_1_0 => {
+ let header_len = std::mem::size_of::<EncryptedDataBlobHeader>();
+ let head = unsafe {
+ (&self.raw_data[..header_len]).read_le_value::<EncryptedDataBlobHeader>()?
+ };
- if let Some(config) = config {
- let data = if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 {
- Self::decode_compressed_chunk(
- config,
- &self.raw_data[header_len..],
- &head.iv,
- &head.tag,
- )?
+ if let Some(config) = config {
+ let data = if magic == ENCR_COMPR_BLOB_MAGIC_1_0 {
+ Self::decode_compressed_chunk(
+ config,
+ &self.raw_data[header_len..],
+ &head.iv,
+ &head.tag,
+ )?
+ } else {
+ Self::decode_uncompressed_chunk(
+ config,
+ &self.raw_data[header_len..],
+ &head.iv,
+ &head.tag,
+ )?
+ };
+ (data, Some(config))
} else {
- Self::decode_uncompressed_chunk(
- config,
- &self.raw_data[header_len..],
- &head.iv,
- &head.tag,
- )?
- };
- if let Some(digest) = digest {
- Self::verify_digest(&data, Some(config), digest)?;
+ bail!("unable to decrypt blob - missing CryptConfig");
}
- Ok(data)
- } else {
- bail!("unable to decrypt blob - missing CryptConfig");
}
- } else {
- bail!("Invalid blob magic number.");
+ _ => bail!("Invalid blob magic number."),
+ };
+
+ if let Some(digest) = digest {
+ Self::verify_digest(&data, crypt_config, digest)?;
}
+
+ Ok(data)
}
/// Load data blob via given sync ``reader`` and verify its CRC
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method
2026-05-07 13:01 ` [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method Christian Ebner
@ 2026-07-22 11:31 ` Robert Obkircher
0 siblings, 0 replies; 11+ messages in thread
From: Robert Obkircher @ 2026-07-22 11:31 UTC (permalink / raw)
To: Christian Ebner; +Cc: pbs-devel
> Improve code style and readability by using a single match statement
> instead of chained if statements and deduplicate the common digest
> verificaton, performed on the decoded blob data.
>
> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
>
> diff --git a/pbs-datastore/src/data_blob.rs b/pbs-datastore/src/data_blob.rs
> index 465bcb280..9fd791af5 100644
> --- a/pbs-datastore/src/data_blob.rs
> +++ b/pbs-datastore/src/data_blob.rs
> @@ -180,57 +180,57 @@ impl DataBlob {
> config: Option<&CryptConfig>,
> digest: Option<&[u8; 32]>,
> ) -> Result<Vec<u8>, Error> {
> - let magic = self.magic();
> + let magic = *self.magic();
>
> - if magic == &UNCOMPRESSED_BLOB_MAGIC_1_0 {
> - let data_start = std::mem::size_of::<DataBlobHeader>();
> - let data = self.raw_data[data_start..].to_vec();
> - if let Some(digest) = digest {
> - Self::verify_digest(&data, None, digest)?;
> + let (data, crypt_config) = match magic {
> + UNCOMPRESSED_BLOB_MAGIC_1_0 => {
> + let data_start = std::mem::size_of::<DataBlobHeader>();
> + let data = self.raw_data[data_start..].to_vec();
> + (data, None)
> }
> - Ok(data)
> - } else if magic == &COMPRESSED_BLOB_MAGIC_1_0 {
> - let data_start = std::mem::size_of::<DataBlobHeader>();
> - let mut reader = &self.raw_data[data_start..];
> - let data = zstd::stream::decode_all(&mut reader)?;
> - // zstd::block::decompress is about 10% slower
> - // let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
> - if let Some(digest) = digest {
> - Self::verify_digest(&data, None, digest)?;
> + COMPRESSED_BLOB_MAGIC_1_0 => {
> + let data_start = std::mem::size_of::<DataBlobHeader>();
> + let mut reader = &self.raw_data[data_start..];
> + let data = zstd::stream::decode_all(&mut reader)?;
> + // zstd::block::decompress is about 10% slower
> + // let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
> + (data, None)
> }
> - Ok(data)
> - } else if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0 {
> - let header_len = std::mem::size_of::<EncryptedDataBlobHeader>();
> - let head = unsafe {
> - (&self.raw_data[..header_len]).read_le_value::<EncryptedDataBlobHeader>()?
> - };
> + ENCR_COMPR_BLOB_MAGIC_1_0 | ENCRYPTED_BLOB_MAGIC_1_0 => {
> + let header_len = std::mem::size_of::<EncryptedDataBlobHeader>();
> + let head = unsafe {
> + (&self.raw_data[..header_len]).read_le_value::<EncryptedDataBlobHeader>()?
> + };
>
> - if let Some(config) = config {
> - let data = if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 {
> - Self::decode_compressed_chunk(
> - config,
> - &self.raw_data[header_len..],
> - &head.iv,
> - &head.tag,
> - )?
> + if let Some(config) = config {
nit: this could be `let Some(config) = config else { bail!(..); };`
to reduce indentation.
--
Robert Obkircher <r.obkircher@proxmox.com>
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox-backup v2 3/5] client: backup writer: pass no crypt config to manifest blob decoder
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 1/5] datastore: data blob: refactor crypt mode method Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 2/5] datastore: data blob: refactor decoding method Christian Ebner
@ 2026-05-07 13:01 ` Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 4/5] client: allow skipping signature check on previous manifest fetching Christian Ebner
` (3 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Christian Ebner @ 2026-05-07 13:01 UTC (permalink / raw)
To: pbs-devel
Data blobs storing a manifest are never encrypted, so passing the
backup writer crypt config on DataBlob::decode() while fetching the
previous manifest is useless, never used in that case anyways.
Simply do not pass the crypt config to make the code semantically
correct.
No functional changes intended.
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
pbs-client/src/backup_writer.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pbs-client/src/backup_writer.rs b/pbs-client/src/backup_writer.rs
index 49aff3fdd..47f08840e 100644
--- a/pbs-client/src/backup_writer.rs
+++ b/pbs-client/src/backup_writer.rs
@@ -745,8 +745,8 @@ impl BackupWriter {
.await?;
let blob = DataBlob::load_from_reader(&mut &raw_data[..])?;
- // no expected digest available
- let data = blob.decode(self.crypt_config.as_ref().map(Arc::as_ref), None)?;
+ // manifest blobs are never encrypted and no expected digest available
+ let data = blob.decode(None, None)?;
let manifest =
BackupManifest::from_data(&data[..], self.crypt_config.as_ref().map(Arc::as_ref))?;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH proxmox-backup v2 4/5] client: allow skipping signature check on previous manifest fetching
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
` (2 preceding siblings ...)
2026-05-07 13:01 ` [PATCH proxmox-backup v2 3/5] client: backup writer: pass no crypt config to manifest blob decoder Christian Ebner
@ 2026-05-07 13:01 ` Christian Ebner
2026-05-07 13:01 ` [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches Christian Ebner
` (2 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Christian Ebner @ 2026-05-07 13:01 UTC (permalink / raw)
To: pbs-devel
Extends the BackupWriter method to download the previous manifest
from a PBS instance, by a flag which allows skipping the signature
check even if the backup writer has a crypt_config set.
Silences misleading logs during encrypting push sync jobs.
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
pbs-client/src/backup_writer.rs | 19 ++++++++++++++-----
proxmox-backup-client/src/main.rs | 2 +-
src/server/push.rs | 2 +-
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/pbs-client/src/backup_writer.rs b/pbs-client/src/backup_writer.rs
index 47f08840e..b60d71d3f 100644
--- a/pbs-client/src/backup_writer.rs
+++ b/pbs-client/src/backup_writer.rs
@@ -735,8 +735,14 @@ impl BackupWriter {
})
}
- /// Download backup manifest (index.json) of last backup
- pub async fn download_previous_manifest(&self) -> Result<BackupManifest, Error> {
+ /// Download backup manifest (index.json) of last backup.
+ ///
+ /// If `check_signature` is set and the writer stores a crypt config,
+ /// also checks the manifest's signature.
+ pub async fn download_previous_manifest(
+ &self,
+ check_signature: bool,
+ ) -> Result<BackupManifest, Error> {
let mut raw_data = Vec::with_capacity(64 * 1024);
let param = json!({ "archive-name": MANIFEST_BLOB_NAME.to_string() });
@@ -748,10 +754,13 @@ impl BackupWriter {
// manifest blobs are never encrypted and no expected digest available
let data = blob.decode(None, None)?;
- let manifest =
- BackupManifest::from_data(&data[..], self.crypt_config.as_ref().map(Arc::as_ref))?;
+ let crypt_config = if check_signature {
+ self.crypt_config.as_ref().map(Arc::as_ref)
+ } else {
+ None
+ };
- Ok(manifest)
+ BackupManifest::from_data(&data[..], crypt_config)
}
// We have no `self` here for `h2` and `verbose`, the only other arg "common" with 1 other
diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs
index 5e8bb5393..c96db321f 100644
--- a/proxmox-backup-client/src/main.rs
+++ b/proxmox-backup-client/src/main.rs
@@ -1064,7 +1064,7 @@ async fn create_backup(
};
let previous_manifest = if download_previous_manifest {
- match client.download_previous_manifest().await {
+ match client.download_previous_manifest(true).await {
Ok(previous_manifest) => {
match previous_manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref)) {
Ok(()) => Some(Arc::new(previous_manifest)),
diff --git a/src/server/push.rs b/src/server/push.rs
index dac62c84a..afa78b751 100644
--- a/src/server/push.rs
+++ b/src/server/push.rs
@@ -1131,7 +1131,7 @@ pub(crate) async fn push_snapshot(
let mut previous_manifest = None;
// Use manifest of previous snapshots in group on target for chunk upload deduplication
if fetch_previous_manifest {
- match backup_writer.download_previous_manifest().await {
+ match backup_writer.download_previous_manifest(false).await {
Ok(manifest) => previous_manifest = Some(Arc::new(manifest)),
Err(err) => {
log_sender
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
` (3 preceding siblings ...)
2026-05-07 13:01 ` [PATCH proxmox-backup v2 4/5] client: allow skipping signature check on previous manifest fetching Christian Ebner
@ 2026-05-07 13:01 ` Christian Ebner
2026-07-22 11:31 ` Robert Obkircher
2026-07-21 11:03 ` [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
2026-07-22 11:31 ` Robert Obkircher
6 siblings, 1 reply; 11+ messages in thread
From: Christian Ebner @ 2026-05-07 13:01 UTC (permalink / raw)
To: pbs-devel
During push sync jobs with a given active encryption key, the key is
loaded for the backup writer, used to encrypt the source snapshot on
the fly. As optimization, the backup writer deduplicates chunks
already present in the previous snapshot by loading them from the
index files as stored in the previous manifest.
Reuse of the previous manifest and index must however only happen if:
- The previous manifest is not encrypted, neither will push encrypt.
- The previous manifest is encrypted and passes signature
verification using the same key to be used during push encryption.
Since these checks are now performed when loading the previous
manifest, drop redundant and now useless checks performed before
loading the chunks from index files.
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
src/server/push.rs | 81 ++++++++++++++++++++++++++++++++++------------
1 file changed, 61 insertions(+), 20 deletions(-)
diff --git a/src/server/push.rs b/src/server/push.rs
index afa78b751..4495b68ed 100644
--- a/src/server/push.rs
+++ b/src/server/push.rs
@@ -979,25 +979,12 @@ pub(crate) async fn push_group(
}
async fn load_previous_snapshot_known_chunks(
- params: &PushParameters,
upload_options: &UploadOptions,
backup_writer: &BackupWriter,
archive_name: &BackupArchiveName,
known_chunks: Arc<Mutex<HashSet<[u8; 32]>>>,
) {
if let Some(manifest) = upload_options.previous_manifest.as_ref() {
- if let Some((_id, crypt_config)) = ¶ms.crypt_config {
- // no fingerprint in previous manifest -> no reuse possible
- let Ok(Some(fingerprint)) = manifest.fingerprint() else {
- return;
- };
-
- if *fingerprint.bytes() != crypt_config.fingerprint() {
- // key mismatch -> no reuse possible
- return;
- }
- }
-
// Add known chunks, ignore errors since archive might not be present and it is better
// to proceed on unrelated errors than to fail here.
match archive_name.archive_type() {
@@ -1128,18 +1115,73 @@ pub(crate) async fn push_snapshot(
.await
.with_context(|| prefix.to_string())?;
- let mut previous_manifest = None;
// Use manifest of previous snapshots in group on target for chunk upload deduplication
- if fetch_previous_manifest {
- match backup_writer.download_previous_manifest(false).await {
- Ok(manifest) => previous_manifest = Some(Arc::new(manifest)),
+ let previous_manifest = if !fetch_previous_manifest {
+ None
+ } else {
+ let result = backup_writer
+ .download_previous_manifest(false)
+ .await
+ .map_err(|err| err.context("failed to download"))
+ .and_then(|manifest| {
+ let fingerprint = manifest
+ .fingerprint()
+ .context("failed getting fingerprint")?;
+
+ let Some(manifest_key_fp) = fingerprint else {
+ if encrypt_using_key.is_none() {
+ return Ok(Arc::new(manifest));
+ }
+ bail!("manifest not encrypted");
+ };
+
+ let signed_only = manifest
+ .files()
+ .iter()
+ .all(|f| f.chunk_crypt_mode() == CryptMode::None);
+
+ let Some((_key_id, crypt_config)) = &encrypt_using_key else {
+ if signed_only {
+ return Ok(Arc::new(manifest));
+ }
+ bail!("manifest encrypted but no encryption key configured");
+ };
+
+ if *manifest_key_fp.bytes() == crypt_config.fingerprint() {
+ if let Some(expected_signature) = &manifest.signature {
+ let calculated_signature = manifest
+ .signature(crypt_config)
+ .context("signature calculation failed")?;
+ if hex::encode(calculated_signature) == *expected_signature {
+ return Ok(Arc::new(manifest));
+ }
+ bail!("signature mismatch with configured encryption key");
+ }
+ bail!("manifest not signed with configured encryption key");
+ }
+ bail!(
+ "encryption key of previous snapshot does not match configured encryption key"
+ );
+ });
+
+ match result {
+ Ok(prev) => {
+ log_sender
+ .log(
+ Level::INFO,
+ format!("{prefix}: Reusing previous manifest for deduplication"),
+ )
+ .await?;
+ Some(prev)
+ }
Err(err) => {
log_sender
.log(
Level::INFO,
- format!("{prefix}: Could not download previous manifest - {err}"),
+ format!("{prefix}: Skip reuse of previous manifest - {err:#}"),
)
- .await?
+ .await?;
+ None
}
}
};
@@ -1184,7 +1226,6 @@ pub(crate) async fn push_snapshot(
};
load_previous_snapshot_known_chunks(
- params,
&upload_options,
&backup_writer,
&archive_name,
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches
2026-05-07 13:01 ` [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches Christian Ebner
@ 2026-07-22 11:31 ` Robert Obkircher
2026-07-22 14:49 ` Christian Ebner
0 siblings, 1 reply; 11+ messages in thread
From: Robert Obkircher @ 2026-07-22 11:31 UTC (permalink / raw)
To: Christian Ebner; +Cc: pbs-devel
> During push sync jobs with a given active encryption key, the key is
> loaded for the backup writer, used to encrypt the source snapshot on
> the fly. As optimization, the backup writer deduplicates chunks
> already present in the previous snapshot by loading them from the
> index files as stored in the previous manifest.
>
> Reuse of the previous manifest and index must however only happen if:
> - The previous manifest is not encrypted, neither will push encrypt.
> - The previous manifest is encrypted and passes signature
> verification using the same key to be used during push encryption.
>
> Since these checks are now performed when loading the previous
> manifest, drop redundant and now useless checks performed before
> loading the chunks from index files.
>
> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
>
> diff --git a/src/server/push.rs b/src/server/push.rs
> index 17fa605a4..6df83cbd8 100644
> --- a/src/server/push.rs
> +++ b/src/server/push.rs
> @@ -979,25 +979,12 @@ pub(crate) async fn push_group(
> }
>
> async fn load_previous_snapshot_known_chunks(
> - params: &PushParameters,
> upload_options: &UploadOptions,
> backup_writer: &BackupWriter,
> archive_name: &BackupArchiveName,
> known_chunks: Arc<Mutex<HashSet<[u8; 32]>>>,
> ) {
> if let Some(manifest) = upload_options.previous_manifest.as_ref() {
> - if let Some((_id, crypt_config)) = ¶ms.crypt_config {
> - // no fingerprint in previous manifest -> no reuse possible
> - let Ok(Some(fingerprint)) = manifest.fingerprint() else {
> - return;
> - };
> -
> - if *fingerprint.bytes() != crypt_config.fingerprint() {
> - // key mismatch -> no reuse possible
> - return;
> - }
> - }
> -
> // Add known chunks, ignore errors since archive might not be present and it is better
> // to proceed on unrelated errors than to fail here.
> match archive_name.archive_type() {
> @@ -1128,18 +1115,73 @@ pub(crate) async fn push_snapshot(
> .await
> .with_context(|| prefix.to_string())?;
>
> - let mut previous_manifest = None;
> // Use manifest of previous snapshots in group on target for chunk upload deduplication
> - if fetch_previous_manifest {
> - match backup_writer.download_previous_manifest(false).await {
> - Ok(manifest) => previous_manifest = Some(Arc::new(manifest)),
> + let previous_manifest = if !fetch_previous_manifest {
> + None
> + } else {
> + let result = backup_writer
> + .download_previous_manifest(false)
> + .await
> + .map_err(|err| err.context("failed to download"))
nit: this could instead call Context::context without the map_err
> + .and_then(|manifest| {
> + let fingerprint = manifest
> + .fingerprint()
> + .context("failed getting fingerprint")?;
> +
> + let Some(manifest_key_fp) = fingerprint else {
> + if encrypt_using_key.is_none() {
> + return Ok(Arc::new(manifest));
What if encrypt_using_key is None because the local chunks are already
encrypted? Shouldn't we expect a fingerprint in that case?
(I'm really not sure if I understand this)
> + }
> + bail!("manifest not encrypted");
This error message confused me initially because, according to the 3rd
commit, manifest blobs are never encrypted. Should it maybe be "backup
not encrypted" or "missing fingerprint"?
> + };
> +
> + let signed_only = manifest
> + .files()
> + .iter()
> + .all(|f| f.chunk_crypt_mode() == CryptMode::None);
> +
> + let Some((_key_id, crypt_config)) = &encrypt_using_key else {
> + if signed_only {
> + return Ok(Arc::new(manifest));
> + }
> + bail!("manifest encrypted but no encryption key configured");
My previous two comments might also apply here.
--
Robert Obkircher <r.obkircher@proxmox.com>
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches
2026-07-22 11:31 ` Robert Obkircher
@ 2026-07-22 14:49 ` Christian Ebner
0 siblings, 0 replies; 11+ messages in thread
From: Christian Ebner @ 2026-07-22 14:49 UTC (permalink / raw)
To: Robert Obkircher; +Cc: pbs-devel
On 7/22/26 1:30 PM, Robert Obkircher wrote:
>> During push sync jobs with a given active encryption key, the key is
>> loaded for the backup writer, used to encrypt the source snapshot on
>> the fly. As optimization, the backup writer deduplicates chunks
>> already present in the previous snapshot by loading them from the
>> index files as stored in the previous manifest.
>>
>> Reuse of the previous manifest and index must however only happen if:
>> - The previous manifest is not encrypted, neither will push encrypt.
>> - The previous manifest is encrypted and passes signature
>> verification using the same key to be used during push encryption.
>>
>> Since these checks are now performed when loading the previous
>> manifest, drop redundant and now useless checks performed before
>> loading the chunks from index files.
>>
>> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
>>
>> diff --git a/src/server/push.rs b/src/server/push.rs
>> index 17fa605a4..6df83cbd8 100644
>> --- a/src/server/push.rs
>> +++ b/src/server/push.rs
>> @@ -979,25 +979,12 @@ pub(crate) async fn push_group(
>> }
>>
>> async fn load_previous_snapshot_known_chunks(
>> - params: &PushParameters,
>> upload_options: &UploadOptions,
>> backup_writer: &BackupWriter,
>> archive_name: &BackupArchiveName,
>> known_chunks: Arc<Mutex<HashSet<[u8; 32]>>>,
>> ) {
>> if let Some(manifest) = upload_options.previous_manifest.as_ref() {
>> - if let Some((_id, crypt_config)) = ¶ms.crypt_config {
>> - // no fingerprint in previous manifest -> no reuse possible
>> - let Ok(Some(fingerprint)) = manifest.fingerprint() else {
>> - return;
>> - };
>> -
>> - if *fingerprint.bytes() != crypt_config.fingerprint() {
>> - // key mismatch -> no reuse possible
>> - return;
>> - }
>> - }
>> -
>> // Add known chunks, ignore errors since archive might not be present and it is better
>> // to proceed on unrelated errors than to fail here.
>> match archive_name.archive_type() {
>> @@ -1128,18 +1115,73 @@ pub(crate) async fn push_snapshot(
>> .await
>> .with_context(|| prefix.to_string())?;
>>
>> - let mut previous_manifest = None;
>> // Use manifest of previous snapshots in group on target for chunk upload deduplication
>> - if fetch_previous_manifest {
>> - match backup_writer.download_previous_manifest(false).await {
>> - Ok(manifest) => previous_manifest = Some(Arc::new(manifest)),
>> + let previous_manifest = if !fetch_previous_manifest {
>> + None
>> + } else {
>> + let result = backup_writer
>> + .download_previous_manifest(false)
>> + .await
>> + .map_err(|err| err.context("failed to download"))
> nit: this could instead call Context::context without the map_err
Ack, will adapt.
>> + .and_then(|manifest| {
>> + let fingerprint = manifest
>> + .fingerprint()
>> + .context("failed getting fingerprint")?;
>> +
>> + let Some(manifest_key_fp) = fingerprint else {
>> + if encrypt_using_key.is_none() {
>> + return Ok(Arc::new(manifest));
> What if encrypt_using_key is None because the local chunks are already
> encrypted? Shouldn't we expect a fingerprint in that case?
The fingerprint here is the one of the previous manifest on the remote,
so if that has no fingerprint (not encrypted), and we have no key to
encrypted (encrypt_using_key == None) it is fine to use the manifest for
de-duplication during chunk upload of the contained matching index
files, analog to an incremental backup. So if the local snapshot to be
pushed is already encrypted (not on the fly encrypted), it will be
allowed to reuse the manifest, but there will be no matching chunk
digest. Just like for a regular push without any server side encryption.
At least that is the intended behavior.
>
> (I'm really not sure if I understand this)
>> + }
>> + bail!("manifest not encrypted");
> This error message confused me initially because, according to the 3rd
> commit, manifest blobs are never encrypted. Should it maybe be "backup
> not encrypted" or "missing fingerprint"?
Will improve, thanks!
>> + };
>> +
>> + let signed_only = manifest
>> + .files()
>> + .iter()
>> + .all(|f| f.chunk_crypt_mode() == CryptMode::None);
>> +
>> + let Some((_key_id, crypt_config)) = &encrypt_using_key else {
>> + if signed_only {
>> + return Ok(Arc::new(manifest));
>> + }
>> + bail!("manifest encrypted but no encryption key configured");
> My previous two comments might also apply here.
>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
` (4 preceding siblings ...)
2026-05-07 13:01 ` [PATCH proxmox-backup v2 5/5] sync: push: gracefully handle previous manifest signature mismatches Christian Ebner
@ 2026-07-21 11:03 ` Christian Ebner
2026-07-22 11:31 ` Robert Obkircher
6 siblings, 0 replies; 11+ messages in thread
From: Christian Ebner @ 2026-07-21 11:03 UTC (permalink / raw)
To: pbs-devel
gentle ping, patch series still applies!
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs
2026-05-07 13:01 [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
` (5 preceding siblings ...)
2026-07-21 11:03 ` [PATCH proxmox-backup v2 0/5] restrict previous manifest reuse checks for push sync jobs Christian Ebner
@ 2026-07-22 11:31 ` Robert Obkircher
6 siblings, 0 replies; 11+ messages in thread
From: Robert Obkircher @ 2026-07-22 11:31 UTC (permalink / raw)
To: Christian Ebner; +Cc: pbs-devel
On Thu, 07 May 2026 15:01:30 +0200, Christian Ebner <c.ebner@proxmox.com> wrote:
> restrict previous manifest reuse checks for push sync jobs
>
> Restrict and rework the checks for reuse of previous snapshots
> manifest's for push sync jobs, in particular with key and signature
> mismatches for sync jobs with active encryption key.
> In particular, only allow reuse of the previous manifest if either
> the previous manifest is not encrypted, and the push sync will also
> not encrypt or the previous manifest is encrypted with the matching
> active encryption key configured for the sync job, the manifest's
> signature being verified with that key.
>
> [...]
Reviewed-by: Robert Obkircher <r.obkircher@proxmox.com>
--
Robert Obkircher <r.obkircher@proxmox.com>
^ permalink raw reply [flat|nested] 11+ messages in thread