From 10daa9b74dd1222d46300f1916c36e507f489a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 11 Oct 2026 21:49:44 +0500 Subject: [PATCH 1/2] Resolve startup fixture roots before copying candidate files --- crates/coa-core/src/release_schema.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/coa-core/src/release_schema.rs b/crates/coa-core/src/release_schema.rs index c79f57c..7fc7f5f 100644 --- a/crates/coa-core/src/release_schema.rs +++ b/crates/coa-core/src/release_schema.rs @@ -157,6 +157,10 @@ pub fn validate_startup(fixture: &Path, tree: &Path, base: &Path, trusted_key: & || std::fs::read(fixture.join(MARKER)).ok().as_deref() != Some(b"disposable release-schema fixture") { return Err(Error::Invalid("Startup qualification requires a disposable release-schema fixture.".into())); } + let fixture_root = fsx::canonicalize_lenient(fixture)?; + let fixture = fixture_root.as_path(); + let tree_root = fsx::canonicalize_lenient(tree)?; + let tree = tree_root.as_path(); let metadata = crate::registry::metadata_dir_for(fixture)?; if metadata.exists() { return Err(Error::Invalid("Startup fixture already has Manager metadata; use a fresh fixture.".into())); } schema_check::require_release_contract(tree)?; @@ -190,7 +194,11 @@ pub fn validate_startup(fixture: &Path, tree: &Path, base: &Path, trusted_key: & package::extract_selected(base, &manifest, fixture, &|path| path.starts_with("Data/"), &|_, _| {})?; for relative in files { let target = fsx::ensure_within(fixture, &fixture.join(&relative))?; - fsx::atomic_write(&target, &std::fs::read(tree.join(relative))?)?; + fsx::atomic_write(&target, &std::fs::read(tree.join(&relative))?)?; + let expected = fsx::sha256_file(&tree.join(relative))?; + if fsx::sha256_file(&target)? != expected { + return Err(Error::Invalid("Startup candidate copy failed verification.".into())); + } } let meta = crate::registry::InstallMeta::new(crate::registry::InstallKind::New, fixture); crate::registry::MetaDir::create(fixture, &meta)?; @@ -233,6 +241,18 @@ mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; use ed25519_dalek::{Signer, SigningKey}; + #[test] + fn relative_startup_fixture_targets_replace_the_original_location() { + let directory = tempfile::Builder::new().prefix("coa-schema-fixture-path-test-").tempdir_in(".").unwrap(); + let relative = directory.path(); + fsx::atomic_write(&relative.join("Core/worldserver.exe"), b"old").unwrap(); + let root = fsx::canonicalize_lenient(relative).unwrap(); + let target = fsx::ensure_within(&root, &root.join("Core/worldserver.exe")).unwrap(); + fsx::atomic_write(&target, b"candidate").unwrap(); + assert_eq!(std::fs::read(relative.join("Core/worldserver.exe")).unwrap(), b"candidate"); + assert!(!root.join(relative).join("Core/worldserver.exe").exists()); + } + #[test] fn startup_qualification_refuses_real_server_folders_before_writes() { let directory = tempfile::tempdir().unwrap(); From 7e170744bf40490e8f5427fd212f52f763e98296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 11 Oct 2026 21:50:09 +0500 Subject: [PATCH 2/2] Exercise an explicitly relative fixture path in regression --- crates/coa-core/src/release_schema.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/coa-core/src/release_schema.rs b/crates/coa-core/src/release_schema.rs index 7fc7f5f..e7aa151 100644 --- a/crates/coa-core/src/release_schema.rs +++ b/crates/coa-core/src/release_schema.rs @@ -244,7 +244,8 @@ mod tests { #[test] fn relative_startup_fixture_targets_replace_the_original_location() { let directory = tempfile::Builder::new().prefix("coa-schema-fixture-path-test-").tempdir_in(".").unwrap(); - let relative = directory.path(); + let current = std::env::current_dir().unwrap(); + let relative = directory.path().strip_prefix(¤t).unwrap(); fsx::atomic_write(&relative.join("Core/worldserver.exe"), b"old").unwrap(); let root = fsx::canonicalize_lenient(relative).unwrap(); let target = fsx::ensure_within(&root, &root.join("Core/worldserver.exe")).unwrap();