From 5f2828b50adadfe2ac51b3d575bd270afe3858ab Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Thu, 16 Jul 2026 14:01:31 +0800
Subject: [PATCH] fix(installer): time out stalled runtime downloads (#108619)
* fix(installer): bound curl download stalls
* chore: format installer timeout tests
* fix(installer): limit timeout to true download stalls
* fix(installer): scope timeout to transfer stalls
---
scripts/install-cli.sh | 6 +++++-
scripts/install.sh | 6 +++++-
test/scripts/install-cli.test.ts | 21 +++++++++++++++++++++
test/scripts/install-sh.test.ts | 21 +++++++++++++++++++++
4 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh
index 2bbdef363d1a..494720d43e27 100755
--- a/scripts/install-cli.sh
+++ b/scripts/install-cli.sh
@@ -137,7 +137,11 @@ download_file() {
detect_downloader
fi
if [[ "$DOWNLOADER" == "curl" ]]; then
- curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
+ # Bound post-connect stalls without imposing a total download duration.
+ curl -fsSL --proto '=https' --tlsv1.2 \
+ --speed-limit 1 --speed-time 30 \
+ --retry 3 --retry-delay 1 --retry-connrefused \
+ -o "$output" "$url"
return
fi
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
diff --git a/scripts/install.sh b/scripts/install.sh
index 43ba90b46d49..435d3445cf05 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -114,7 +114,11 @@ download_file() {
detect_downloader
fi
if [[ "$DOWNLOADER" == "curl" ]]; then
- curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
+ # Bound post-connect stalls without imposing a total download duration.
+ curl -fsSL --proto '=https' --tlsv1.2 \
+ --speed-limit 1 --speed-time 30 \
+ --retry 3 --retry-delay 1 --retry-connrefused \
+ -o "$output" "$url"
return
fi
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts
index af5ea93013d1..a84aab2782f1 100644
--- a/test/scripts/install-cli.test.ts
+++ b/test/scripts/install-cli.test.ts
@@ -41,6 +41,27 @@ function linkRequiredShellTools(bin: string) {
describe("install-cli.sh", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
+ it("bounds stalled curl downloads and propagates timeout failures", () => {
+ const result = runInstallCliShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ curl() {
+ printf 'curl=%s\n' "$*"
+ return 28
+ }
+ DOWNLOADER=curl
+ set +e
+ download_file "https://example.invalid/node.tar.gz" "/tmp/node.tar.gz"
+ printf 'status=%s\n' "$?"
+ `);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("--speed-limit 1 --speed-time 30");
+ expect(result.stdout).not.toContain("--connect-timeout");
+ expect(result.stdout).toContain("--retry 3 --retry-delay 1 --retry-connrefused");
+ expect(result.stdout).toContain("status=28");
+ });
+
it("does not clean an unrelated legacy checkout during the default npm install", () => {
const main = script.slice(script.indexOf("\nmain() {"));
expect(main).not.toContain("cleanup_legacy_submodules");
diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts
index 316f46bf90f6..74273564788f 100644
--- a/test/scripts/install-sh.test.ts
+++ b/test/scripts/install-sh.test.ts
@@ -82,6 +82,27 @@ describe("install.sh", () => {
}
});
+ it("bounds stalled curl downloads and propagates timeout failures", () => {
+ const result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ curl() {
+ printf 'curl=%s\n' "$*"
+ return 28
+ }
+ DOWNLOADER=curl
+ set +e
+ download_file "https://example.invalid/archive.tgz" "/tmp/archive.tgz"
+ printf 'status=%s\n' "$?"
+ `);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("--speed-limit 1 --speed-time 30");
+ expect(result.stdout).not.toContain("--connect-timeout");
+ expect(result.stdout).toContain("--retry 3 --retry-delay 1 --retry-connrefused");
+ expect(result.stdout).toContain("status=28");
+ });
+
it("runs apt-get through noninteractive wrappers", () => {
expect(script).toContain("apt_get()");
expect(script).toContain('DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"');