Refuse stores marked by a newer release (release/2026) #247

Merged
jcoffey-dev merged 1 commits from fix/store-version-mark-2026 into release/2026 2026-10-30 03:52:50 +00:00
4 changed files with 138 additions and 5 deletions

No files matched your search

+10
View File
@@ -106,6 +106,16 @@ Schema history:
pub const DATABASE_SCHEMA_VERSION: u32 = 6;
// inbuxa: the layout of the store as our own line keeps it, beside the
// schema version above (which stays at 0.16's, so a Stalwart 0.16 store
// still opens). Kept in the fork's subspace under `Vs`; a store without it
// was last opened by a release from before the mark, or by Stalwart, and
// gets it on start. A release refuses a store marked newer than its own.
//
// 1 - the 0.16 layout plus the fork's own subspace (2026.10.x, 27.1)
pub const INBUXA_STORE_VERSION: u32 = 1;
pub const INBUXA_STORE_VERSION_KEY: &[u8] = b"Vs";
pub const LONG_1D_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
pub const LONG_1Y_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365);
+50 -5
View File
@@ -9,9 +9,9 @@
#![warn(clippy::large_futures)]
use crate::v016::migrate_v0_16;
use common::{DATABASE_SCHEMA_VERSION, Server};
use common::{DATABASE_SCHEMA_VERSION, INBUXA_STORE_VERSION, INBUXA_STORE_VERSION_KEY, Server};
use store::{
IterateParams, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN,
IterateParams, SUBSPACE_INBUXA, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN,
SUBSPACE_REPORT_OUT, SerializeInfallible,
write::{AnyClass, AnyKey, BatchBuilder, ValueClass},
};
@@ -21,6 +21,15 @@ pub mod destroy;
pub mod v016;
pub async fn try_migrate(server: &Server) -> trc::Result<()> {
// inbuxa: a store a newer release has changed is refused before anything
// below writes to it.
let store_version = store_version(server).await?;
if let Some(version) = store_version.filter(|version| *version > INBUXA_STORE_VERSION) {
abort(&format!(
"The data store was upgraded by a newer release (store version {version}, this release reads up to {INBUXA_STORE_VERSION}). Run that release again, or restore the backup taken before the upgrade, see https://docs.inbuxa.org/install/migrating/"
));
}
// inbuxa: before the version check, which returns early on a current
// store, and before migrate_v0_16, which reads the renamed key.
rename_spam_blobs(server).await?;
@@ -36,7 +45,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
{
Some(DATABASE_SCHEMA_VERSION) => {
if !std::env::var("DANGER_FORCE_MIGRATE").is_ok_and(|v| v == "1") {
return Ok(());
return mark_store(server, store_version).await;
}
}
Some(0..=4) => {
@@ -63,7 +72,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
_ => {
if is_new_install(server).await.caused_by(trc::location!())? {
write_schema_version(server).await?;
return Ok(());
return mark_store(server, store_version).await;
} else {
abort(concat!(
"You must first upgrade to version 0.15, please read ",
@@ -74,7 +83,43 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
}
migrate_v0_16(server).await?;
write_schema_version(server).await
write_schema_version(server).await?;
mark_store(server, store_version).await
}
async fn store_version(server: &Server) -> trc::Result<Option<u32>> {
server
.store()
.get_value::<u32>(AnyKey {
subspace: SUBSPACE_INBUXA,
key: INBUXA_STORE_VERSION_KEY.to_vec(),
})
.await
.caused_by(trc::location!())
}
// inbuxa: written once, when a store has no mark yet
async fn mark_store(server: &Server, store_version: Option<u32>) -> trc::Result<()> {
if store_version.is_some() {
return Ok(());
}
let mut batch = BatchBuilder::new();
batch.set(
ValueClass::Any(AnyClass {
subspace: SUBSPACE_INBUXA,
key: INBUXA_STORE_VERSION_KEY.to_vec(),
}),
INBUXA_STORE_VERSION.serialize(),
);
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
Ok(())
}
async fn write_schema_version(server: &Server) -> trc::Result<()> {
+2
View File
@@ -38,6 +38,8 @@ pub mod smtp;
#[cfg(test)]
pub mod store;
#[cfg(test)]
pub mod store_version; // inbuxa: INBUXA_STORE_VERSION
#[cfg(test)]
pub mod system;
#[cfg(test)]
pub mod telemetry;
+76
View File
@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! inbuxa: the store version mark. A store without it gets it on start, a
//! marked store is left alone, and a store marked by a newer release is
//! refused before anything is written to it.
use crate::utils::server::TestServerBuilder;
use common::{INBUXA_STORE_VERSION, INBUXA_STORE_VERSION_KEY, Server};
use futures::FutureExt;
use std::panic::AssertUnwindSafe;
use store::{
SUBSPACE_INBUXA, SerializeInfallible,
write::{AnyClass, AnyKey, BatchBuilder, ValueClass},
};
async fn read_mark(server: &Server) -> Option<u32> {
server
.store()
.get_value::<u32>(AnyKey {
subspace: SUBSPACE_INBUXA,
key: INBUXA_STORE_VERSION_KEY.to_vec(),
})
.await
.unwrap()
}
async fn write_mark(server: &Server, version: Option<u32>) {
let class = ValueClass::Any(AnyClass {
subspace: SUBSPACE_INBUXA,
key: INBUXA_STORE_VERSION_KEY.to_vec(),
});
let mut batch = BatchBuilder::new();
match version {
Some(version) => batch.set(class, version.serialize()),
None => batch.clear(class),
};
server.store().write(batch.build_all()).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn store_version() {
let test = TestServerBuilder::new("store_version")
.await
.disable_services()
.build()
.await;
let server = &test.server;
println!("Running store version tests...");
// A store from before the mark (or from Stalwart) gets it on start.
write_mark(server, None).await;
migration::try_migrate(server).await.unwrap();
assert_eq!(read_mark(server).await, Some(INBUXA_STORE_VERSION));
// Starting again leaves it as it is.
migration::try_migrate(server).await.unwrap();
assert_eq!(read_mark(server).await, Some(INBUXA_STORE_VERSION));
// A store marked by a newer release is refused, and the mark stays.
let newer = INBUXA_STORE_VERSION + 1;
write_mark(server, Some(newer)).await;
let result = AssertUnwindSafe(migration::try_migrate(server))
.catch_unwind()
.await;
assert!(result.is_err(), "a store marked newer must be refused");
assert_eq!(read_mark(server).await, Some(newer));
// Put back, so the store is usable again.
write_mark(server, Some(INBUXA_STORE_VERSION)).await;
migration::try_migrate(server).await.unwrap();
}