fix(torrents): prevent unmanaged followed child GIDs

This commit is contained in:
NimBold
2026-08-02 22:55:13 +03:30
parent 7da67c15b5
commit d677f98dd1
3 changed files with 75 additions and 1 deletions
+8
View File
@@ -53,6 +53,11 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar
timeouts are bounded to 1604800 seconds; interval 0 restores Aria2's timeouts are bounded to 1604800 seconds; interval 0 restores Aria2's
response/progress-driven scheduling. Timing is persisted and reapplied when response/progress-driven scheduling. Timing is persisted and reapplied when
a Torrent starts or retries. 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 - Global `bt-max-open-files` control for multi-file Torrents, bounded to
14096 with Aria2's default of 100. The setting is persisted, applied at 14096 with Aria2's default of 100. The setting is persisted, applied at
daemon startup, and updateable through Aria2's global-option RPC; changes 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 1. Aria2 `follow-torrent`/in-memory follow behavior for generic downloads only
if the resulting child-GID ownership model can be represented safely; the if the resulting child-GID ownership model can be represented safely; the
current explicit metadata path intentionally avoids unmapped child jobs. 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; The first implementation in this task was remote `.torrent` metadata intake;
follow-up implementations add stall-timeout control, bounded peer diagnostics, follow-up implementations add stall-timeout control, bounded peer diagnostics,
+51
View File
@@ -3408,6 +3408,27 @@ fn apply_aria2_connection_options(
); );
} }
fn apply_aria2_follow_options(
options: &mut serde_json::Map<String, serde_json::Value>,
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<String, String> { fn format_aria2_torrent_number(value: f64, field: &str) -> Result<String, String> {
if !value.is_finite() || value < 0.0 { if !value.is_finite() || value < 0.0 {
return Err(format!("torrent {field} must be a finite non-negative number")); 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; let conn = effective_aria2_connections(id, payload).await;
apply_aria2_connection_options(&mut options, conn); apply_aria2_connection_options(&mut options, conn);
apply_aria2_follow_options(&mut options, payload);
apply_aria2_torrent_options(&mut options, payload)?; apply_aria2_torrent_options(&mut options, payload)?;
let mt = aria2_attempt_limit(payload.max_tries); let mt = aria2_attempt_limit(payload.max_tries);
options.insert("max-tries".to_string(), serde_json::json!(mt.to_string())); 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] #[test]
fn torrent_encryption_policy_maps_to_one_consistent_aria2_policy() { fn torrent_encryption_policy_maps_to_one_consistent_aria2_policy() {
let cases = [ let cases = [
+16 -1
View File
@@ -23,11 +23,16 @@ pub(crate) enum ProbeFailure {
pub(crate) async fn run_metadata_probe<C: RpcClient + 'static>( pub(crate) async fn run_metadata_probe<C: RpcClient + 'static>(
client: Arc<C>, client: Arc<C>,
source: &str, source: &str,
options: Map<String, Value>, mut options: Map<String, Value>,
metadata_path: &Path, metadata_path: &Path,
timeout: Duration, timeout: Duration,
poll_interval: Duration, poll_interval: Duration,
) -> Result<Vec<u8>, ProbeFailure> { ) -> Result<Vec<u8>, 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 mut cleanup_guard = ProbeCleanupGuard::new(Arc::clone(&client), metadata_path);
let result = match client let result = match client
.call("aria2.addUri", json!([[source], options])) .call("aria2.addUri", json!([[source], options]))
@@ -1018,6 +1023,16 @@ mod tests {
Some(&json!("true")), Some(&json!("true")),
"recorded addUri params: {add_params:?}" "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) tokio::fs::remove_dir_all(&probe_dir)
.await .await
.expect("successful probe fixture should be removable"); .expect("successful probe fixture should be removable");