feat(peer): validate manifests before download mutation
Why: - Remote and UI-echoed file descriptions could reach transaction and storage code one entry at a time, so a hostile late path could mutate earlier files. - Per-file consensus also accepted malformed peer lists and let duplicate rows inflate a source's vote. What: - Add a complete protocol-7 manifest adapter with catalog-root confinement, portable path and alias rules, reserved-path protection, shape and size caps, symlink/reparse inspection, and zero-mutation tests. - Keep download selection in the peer core, validate every peer manifest before consensus, and pass only the validated manifest into storage/orchestration. - Canonicalize locally advertised paths, cap exact chunk receives, and preserve the local-only install fast path. - Record the chosen safety limits and follow-up ownership/catalog decisions. Test Plan: - just clippy - just test - just frontend-test - just build - just fmt (Rust/TOML/Prettier completed; rumdl reports 39 pre-existing issues) - git diff --cached --check
This commit is contained in:
12 files changed
+1376
-137
No files matched your search
@@ -17,7 +17,7 @@ use crate::{
|
||||
InstallOperation,
|
||||
PeerEvent,
|
||||
context::{Ctx, OperationGuard, OperationKind},
|
||||
download::download_game_files,
|
||||
download::{ValidatedDownloadManifest, download_game_files, validate_protocol_v7_descriptions},
|
||||
events,
|
||||
install,
|
||||
local_games::{
|
||||
@@ -217,7 +217,6 @@ pub async fn handle_download_game_files_command(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
install_after_download: bool,
|
||||
) {
|
||||
log::info!("Got PeerCommand::DownloadGameFiles");
|
||||
@@ -232,13 +231,43 @@ pub async fn handle_download_game_files_command(
|
||||
let games_folder = { ctx.game_dir.read().await.clone() };
|
||||
let expected_version = catalog_expected_version(ctx, &id).await;
|
||||
|
||||
// Use majority validation to get trusted file descriptions and peer whitelist
|
||||
let raw_peer_manifests = ctx
|
||||
.peer_game_db
|
||||
.read()
|
||||
.await
|
||||
.expected_version_game_files_for(&id, expected_version.as_deref());
|
||||
let validation_game_id = id.clone();
|
||||
let raw_validation = tokio::task::spawn_blocking(move || {
|
||||
let mut valid = Vec::new();
|
||||
let mut rejected = Vec::new();
|
||||
for (peer_addr, descriptions) in raw_peer_manifests {
|
||||
match validate_protocol_v7_descriptions(&validation_game_id, descriptions) {
|
||||
Ok(descriptions) => valid.push((peer_addr, descriptions)),
|
||||
Err(error) => rejected.push((peer_addr, error.to_string())),
|
||||
}
|
||||
}
|
||||
(valid, rejected)
|
||||
})
|
||||
.await;
|
||||
let (peer_manifests, rejected_manifests) = match raw_validation {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
log::error!("Peer manifest validation task failed for {id}: {error}");
|
||||
send_download_failed(tx_notify_ui, &id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
for (peer_addr, error) in rejected_manifests {
|
||||
log::warn!("Ignoring invalid download manifest from {peer_addr} for {id}: {error}");
|
||||
}
|
||||
|
||||
// Use only complete, individually valid peer manifests for size consensus.
|
||||
let (validated_descriptions, peer_whitelist, file_peer_map) = {
|
||||
match ctx
|
||||
.peer_game_db
|
||||
.read()
|
||||
.await
|
||||
.validate_file_sizes_majority(&id, expected_version.as_deref())
|
||||
.validate_file_sizes_majority_from(&id, &peer_manifests)
|
||||
{
|
||||
Ok((files, peers, file_peer_map)) => {
|
||||
log::info!(
|
||||
@@ -260,24 +289,6 @@ pub async fn handle_download_game_files_command(
|
||||
}
|
||||
};
|
||||
|
||||
let resolved_descriptions = if file_descriptions.is_empty() {
|
||||
validated_descriptions
|
||||
} else {
|
||||
// If user provided specific descriptions, still validate them against majority
|
||||
// but keep user's selection (they might want specific files)
|
||||
file_descriptions
|
||||
};
|
||||
|
||||
if resolved_descriptions.is_empty() {
|
||||
log::error!(
|
||||
"No validated file descriptions available to download game {id}; request metadata first"
|
||||
);
|
||||
if let Err(send_err) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id }) {
|
||||
log::error!("Failed to send DownloadGameFilesFailed event: {send_err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let local_dl_available = {
|
||||
let active_operations = ctx.active_operations.read().await;
|
||||
let catalog = ctx.catalog.read().await;
|
||||
@@ -310,6 +321,42 @@ pub async fn handle_download_game_files_command(
|
||||
return;
|
||||
}
|
||||
|
||||
if validated_descriptions.is_empty() {
|
||||
log::error!(
|
||||
"No validated file descriptions available to download game {id}; request metadata first"
|
||||
);
|
||||
if let Err(send_err) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id }) {
|
||||
log::error!("Failed to send DownloadGameFilesFailed event: {send_err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let manifest_games_folder = games_folder.clone();
|
||||
let manifest_game_id = id.clone();
|
||||
let manifest = tokio::task::spawn_blocking(move || {
|
||||
ValidatedDownloadManifest::from_protocol_v7(
|
||||
&manifest_games_folder,
|
||||
&manifest_game_id,
|
||||
validated_descriptions,
|
||||
&catalog,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let manifest = match manifest {
|
||||
Ok(Ok(manifest)) => manifest,
|
||||
Ok(Err(error)) => {
|
||||
log::error!("Rejected download manifest for {id}: {error}");
|
||||
send_download_failed(tx_notify_ui, &id);
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Download manifest validation task failed for {id}: {error}");
|
||||
send_download_failed(tx_notify_ui, &id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match begin_operation(ctx, tx_notify_ui, &id, OperationKind::Downloading).await {
|
||||
BeginOperationResult::Started => {}
|
||||
BeginOperationResult::AlreadyActive => {
|
||||
@@ -344,9 +391,7 @@ pub async fn handle_download_game_files_command(
|
||||
);
|
||||
|
||||
let result = download_game_files(
|
||||
&download_id,
|
||||
resolved_descriptions,
|
||||
games_folder,
|
||||
manifest,
|
||||
peer_whitelist,
|
||||
file_peer_map,
|
||||
tx_notify_ui_clone.clone(),
|
||||
|
||||
Reference in new issue
Block a user