diff --git a/crates/lanspread-peer/src/install/transaction.rs b/crates/lanspread-peer/src/install/transaction.rs index 5ff0951..6dd382d 100644 --- a/crates/lanspread-peer/src/install/transaction.rs +++ b/crates/lanspread-peer/src/install/transaction.rs @@ -26,6 +26,7 @@ use super::{ }; use crate::{ game_paths::{BACKUP_DIR, INSTALL_OWNED_MARKER, INSTALLING_DIR, LOCAL_DIR}, + local_games::read_version_from_ini_bounded, scoped_blocking::scoped_blocking, state_paths::launch_settings_applied_path, }; @@ -549,12 +550,13 @@ async fn unpack_archives( /// Windows, any reparse point (junctions, mount points). /// /// Extraction is delegated to an external `unrar` process whose output is -/// otherwise trusted verbatim. The archives themselves are BLAKE3-verified -/// against the bundled catalog, so a link would have to originate from the -/// catalog publisher; this audit is defense in depth so that a link can never -/// redirect later launch-time rewrites (`apply_launch_settings_once`) or an -/// uninstall outside the game directory. `unrar` is additionally invoked -/// with `-ol-` so links are skipped at extraction time. +/// otherwise trusted verbatim. Downloaded archives are BLAKE3-verified, but +/// ordinary install intentionally consumes the current root `.eti` set so the +/// S33 local-mutation workflow remains supported. This audit prevents either a +/// catalog archive or a locally supplied archive from redirecting later +/// launch-time rewrites (`apply_launch_settings_once`) or an uninstall outside +/// the game directory. `unrar` is additionally invoked with `-ol-` so links are +/// skipped at extraction time. fn reject_links_in_staging(staging: &Path) -> eyre::Result<()> { let staging = staging.to_path_buf(); scoped_blocking(move || { @@ -914,7 +916,7 @@ fn read_downloaded_version(game_root: &Path) -> Option { if !version_ini_is_regular_file_blocking(game_root) { return None; } - match lanspread_db::db::read_version_from_ini(game_root) { + match read_version_from_ini_bounded(game_root) { Ok(version) => version, Err(err) => { log::warn!( diff --git a/crates/lanspread-peer/src/local_games.rs b/crates/lanspread-peer/src/local_games.rs index 74dcc8f..964fa0b 100644 --- a/crates/lanspread-peer/src/local_games.rs +++ b/crates/lanspread-peer/src/local_games.rs @@ -760,7 +760,7 @@ fn build_game_summary( }) } -fn read_version_from_ini_bounded(game_path: &Path) -> eyre::Result> { +pub(crate) fn read_version_from_ini_bounded(game_path: &Path) -> eyre::Result> { let version_path = game_path.join(VERSION_INI); let Some(bytes) = read_bounded_regular_file(&version_path, MAX_VERSION_INI_BYTES)? else { return Ok(None); diff --git a/crates/lanspread-peer/src/services/transfer.rs b/crates/lanspread-peer/src/services/transfer.rs index 770729d..8099496 100644 --- a/crates/lanspread-peer/src/services/transfer.rs +++ b/crates/lanspread-peer/src/services/transfer.rs @@ -29,7 +29,7 @@ use tokio_util::{ use crate::{ context::PeerCtx, download::open_catalog_file_for_read, - local_games::version_ini_is_regular_file, + local_games::{read_version_from_ini_bounded, version_ini_is_regular_file}, peer::send_game_file_chunk, scoped_blocking::scoped_blocking, stream_install::{send_game_install_stream, send_stream_install_error}, @@ -140,7 +140,7 @@ async fn can_serve_game(ctx: &PeerCtx, game_dir: &std::path::Path, game_id: &str let expected_version_for_read = expected_version.clone(); scoped_blocking(move || { expected_version_for_read.as_deref().is_none_or(|expected| { - lanspread_db::db::read_version_from_ini(&game_root) + read_version_from_ini_bounded(&game_root) .is_ok_and(|version| version.as_deref() == Some(expected)) }) }) @@ -847,6 +847,16 @@ mod tests { std::fs::write(game_root.join("version.ini"), b"20250101") .expect("sentinel should be restored"); + std::fs::OpenOptions::new() + .write(true) + .open(game_root.join("version.ini")) + .expect("sentinel should open") + .set_len(64 * 1024 + 1) + .expect("sentinel should become oversized"); + assert_chunk_rejected(&ctx, content_id, &payload, 0, 7).await; + std::fs::write(game_root.join("version.ini"), b"20250101") + .expect("bounded sentinel should be restored"); + std::fs::write(game_root.join("payload.bin"), b"short") .expect("wrong-sized payload should be written"); assert_chunk_rejected(&ctx, content_id, &payload, 0, 7).await;