From d677f98dd12e42f6dd18dff027df135ce8b627a0 Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 2 Aug 2026 22:55:13 +0330 Subject: [PATCH] fix(torrents): prevent unmanaged followed child GIDs --- TORRENT_FEATURES.md | 8 ++++++ src-tauri/src/queue.rs | 51 ++++++++++++++++++++++++++++++++++ src-tauri/src/torrent_probe.rs | 17 +++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/TORRENT_FEATURES.md b/TORRENT_FEATURES.md index 3558235..457c736 100644 --- a/TORRENT_FEATURES.md +++ b/TORRENT_FEATURES.md @@ -53,6 +53,11 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar timeouts are bounded to 1–604800 seconds; interval 0 restores Aria2's response/progress-driven scheduling. Timing is persisted and reapplied when a Torrent starts or retries. +- Generic Aria2 downloads explicitly disable `follow-torrent` and + `follow-metalink`, so a URL that happens to return Torrent or Metalink + metadata cannot create an unmanaged child GID. Generic follow behavior is + not exposed until parent/child GID ownership is represented across queue + admission, progress, cancellation, retry, and restart recovery. - Global `bt-max-open-files` control for multi-file Torrents, bounded to 1–4096 with Aria2's default of 100. The setting is persisted, applied at daemon startup, and updateable through Aria2's global-option RPC; changes @@ -83,6 +88,9 @@ No remaining Tier 1 items. 1. Aria2 `follow-torrent`/in-memory follow behavior for generic downloads only if the resulting child-GID ownership model can be represented safely; the current explicit metadata path intentionally avoids unmapped child jobs. + Generic `addUri` now forces both follow options to `false` as the safe + default; the child-GID feature remains pending until the end-to-end + ownership model is implemented. The first implementation in this task was remote `.torrent` metadata intake; follow-up implementations add stall-timeout control, bounded peer diagnostics, diff --git a/src-tauri/src/queue.rs b/src-tauri/src/queue.rs index 39782be..8517bb4 100644 --- a/src-tauri/src/queue.rs +++ b/src-tauri/src/queue.rs @@ -3408,6 +3408,27 @@ fn apply_aria2_connection_options( ); } +fn apply_aria2_follow_options( + options: &mut serde_json::Map, + payload: &SpawnPayload, +) { + if !payload.is_torrent { + // A generic addUri can point at a .torrent or Metalink file. Aria2 + // may then create a second, followed child GID, but Firelink + // currently owns exactly one GID per download. Keep that unmanaged + // child lifecycle impossible until parent/child ownership is modeled + // end to end. + options.insert( + "follow-torrent".to_string(), + serde_json::json!("false"), + ); + options.insert( + "follow-metalink".to_string(), + serde_json::json!("false"), + ); + } +} + fn format_aria2_torrent_number(value: f64, field: &str) -> Result { if !value.is_finite() || value < 0.0 { return Err(format!("torrent {field} must be a finite non-negative number")); @@ -3981,6 +4002,7 @@ impl SidecarSpawner for ProductionSpawner { } let conn = effective_aria2_connections(id, payload).await; apply_aria2_connection_options(&mut options, conn); + apply_aria2_follow_options(&mut options, payload); apply_aria2_torrent_options(&mut options, payload)?; let mt = aria2_attempt_limit(payload.max_tries); options.insert("max-tries".to_string(), serde_json::json!(mt.to_string())); @@ -4675,6 +4697,35 @@ mod tests { ); } + #[test] + fn generic_aria2_downloads_disable_followed_child_gids() { + let mut options = serde_json::Map::new(); + apply_aria2_follow_options(&mut options, &SpawnPayload::default()); + + assert_eq!( + options.get("follow-torrent"), + Some(&serde_json::json!("false")) + ); + assert_eq!( + options.get("follow-metalink"), + Some(&serde_json::json!("false")) + ); + } + + #[test] + fn explicit_torrent_downloads_do_not_override_follow_policy() { + let mut options = serde_json::Map::new(); + let payload = SpawnPayload { + is_torrent: true, + ..Default::default() + }; + + apply_aria2_follow_options(&mut options, &payload); + + assert!(!options.contains_key("follow-torrent")); + assert!(!options.contains_key("follow-metalink")); + } + #[test] fn torrent_encryption_policy_maps_to_one_consistent_aria2_policy() { let cases = [ diff --git a/src-tauri/src/torrent_probe.rs b/src-tauri/src/torrent_probe.rs index 64048a8..5f453fe 100644 --- a/src-tauri/src/torrent_probe.rs +++ b/src-tauri/src/torrent_probe.rs @@ -23,11 +23,16 @@ pub(crate) enum ProbeFailure { pub(crate) async fn run_metadata_probe( client: Arc, source: &str, - options: Map, + mut options: Map, metadata_path: &Path, timeout: Duration, poll_interval: Duration, ) -> Result, ProbeFailure> { + // This probe only resolves magnet metadata. It must never allow Aria2 to + // interpret a downloaded metadata file as another child download because + // the probe cleanup guard owns exactly one GID. + options.insert("follow-torrent".to_string(), json!("false")); + options.insert("follow-metalink".to_string(), json!("false")); let mut cleanup_guard = ProbeCleanupGuard::new(Arc::clone(&client), metadata_path); let result = match client .call("aria2.addUri", json!([[source], options])) @@ -1018,6 +1023,16 @@ mod tests { Some(&json!("true")), "recorded addUri params: {add_params:?}" ); + assert_eq!( + options.get("follow-torrent"), + Some(&json!("false")), + "recorded addUri params: {add_params:?}" + ); + assert_eq!( + options.get("follow-metalink"), + Some(&json!("false")), + "recorded addUri params: {add_params:?}" + ); tokio::fs::remove_dir_all(&probe_dir) .await .expect("successful probe fixture should be removable");