From 68f13a9c4e81975f31c800530b2aa7286346118b Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 7 Nov 2026 10:14:52 +0000 Subject: [PATCH] fix: stage release downloads on disk, not in /tmp /tmp is a tmpfs on Debian 13, so a release staged there is charged to the container's memory limit, and without swap nothing can reclaim it. Pulse's 273 MiB archive unpacks to 725 MiB, so its 1 GiB container is OOM-killed or stalls during install, and with the default swap it only gets through by swapping. Stage under /var/tmp instead, which is on disk, as /tmp was on Debian 12. TMPDIR still wins. --- lib/API.txt | 1 + lib/forge.func | 24 ++++++++++++++++++------ tests/forge-appversion-scope.sh | 2 +- tests/forge-clean-install.sh | 18 +++++++++++++++++- tests/release-stamp-pending.sh | 2 +- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/API.txt b/lib/API.txt index 1f12aa5..b830002 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -30,6 +30,7 @@ _forge_scan_older _forge_source_url _forge_stable_filter _forge_strip_v +_forge_workdir _gh_scan_older_releases _github_release_noapi _gl_scan_older_releases diff --git a/lib/forge.func b/lib/forge.func index 337b590..b1a5ebf 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -555,6 +555,18 @@ _cs_clean_target_dir() { fi } +# ------------------------------------------------------------------------------ +# _forge_workdir +# +# A private directory for a download and its extraction, on disk. /tmp is tmpfs +# on Debian 13, so a release staged there counts against the container's memory +# limit and nothing can reclaim it: without swap, a 273 MiB archive that unpacks +# to 725 MiB gets the install OOM-killed in a 1 GiB container. TMPDIR still wins. +# ------------------------------------------------------------------------------ +_forge_workdir() { + mktemp -d -p "${TMPDIR:-/var/tmp}" 2>/dev/null || mktemp -d -p /tmp +} + # ------------------------------------------------------------------------------ # _deploy_source_tarball # @@ -761,7 +773,7 @@ fetch_and_deploy_gh_tag() { fi local tmpdir - tmpdir=$(mktemp -d) || return 1 + tmpdir=$(_forge_workdir) || return 1 trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN local tarball_url="https://github.com/${repo}/archive/refs/tags/${version}.tar.gz" local filename="${app_lc}-${version}.tar.gz" @@ -940,7 +952,7 @@ fetch_and_deploy_gl_tag() { local tarball_url="${gitlab_url}/${repo}/-/archive/${resolved_tag}/${app_lc}-${version_safe}.tar.gz" local tmpdir - tmpdir=$(mktemp -d) || return 1 + tmpdir=$(_forge_workdir) || return 1 trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN local filename="${app_lc}-${version_safe}.tar.gz" @@ -2149,7 +2161,7 @@ fetch_and_deploy_release() { json="$(mktemp /tmp/tools-forge-sel-XXXXXX)" || return 73 local tmpdir unpack_tmp="" - tmpdir="$(mktemp -d)" || { + tmpdir="$(_forge_workdir)" || { rm -f "$json" return 1 } @@ -2341,7 +2353,7 @@ fetch_and_deploy_release() { msg_error "Download failed: $asset_url" return 250 } - unpack_tmp="$(mktemp -d)" + unpack_tmp="$(_forge_workdir)" _deploy_unpacked_archive "$tmpdir/$filename" "$target" "$unpack_tmp" || return else mkdir -p "$target" @@ -2449,7 +2461,7 @@ fetch_and_deploy_from_url() { msg_info "Downloading from $url" local tmpdir - tmpdir=$(mktemp -d) || { + tmpdir=$(_forge_workdir) || { msg_error "Failed to create temporary directory" return 252 } @@ -2512,7 +2524,7 @@ fetch_and_deploy_from_url() { msg_info "Extracting archive to $directory" local unpack_tmp - unpack_tmp=$(mktemp -d) + unpack_tmp=$(_forge_workdir) _deploy_unpacked_archive "$tmpdir/$filename" "$directory" "$unpack_tmp" "$archive_type" || return diff --git a/tests/forge-appversion-scope.sh b/tests/forge-appversion-scope.sh index c9acfa4..57b4d91 100644 --- a/tests/forge-appversion-scope.sh +++ b/tests/forge-appversion-scope.sh @@ -2,7 +2,7 @@ set -uo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" -eval "$(awk '/^(fetch_and_deploy_release|_forge_is_script_app|_forge_descriptor|_forge_strip_v|_forge_previous_version)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^(fetch_and_deploy_release|_forge_workdir|_forge_is_script_app|_forge_descriptor|_forge_strip_v|_forge_previous_version)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" TEST_DIR="$(mktemp -d)" trap 'rm -rf -- "$TEST_DIR"' EXIT diff --git a/tests/forge-clean-install.sh b/tests/forge-clean-install.sh index 247c7d9..4d3d4a5 100644 --- a/tests/forge-clean-install.sh +++ b/tests/forge-clean-install.sh @@ -2,7 +2,7 @@ set -euo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" -eval "$(awk '/^(_cs_clean_target_dir|_deploy_source_tarball|_deploy_unpacked_archive)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^(_cs_clean_target_dir|_deploy_source_tarball|_deploy_unpacked_archive|_forge_workdir)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func")" TEST_DIR="$(mktemp -d)" trap 'rm -rf -- "$TEST_DIR"' EXIT @@ -76,5 +76,21 @@ check "source: no top-level dir leaves the install" "251 $intact" "$(source_tarb check "source: clean install replaces the target" "0 ./app " "$(source_tarball good.tar.gz)" check "source: CLEAN_INSTALL_KEEP survives" "0 ./app ./data/db " "$(CLEAN_INSTALL_KEEP="data" source_tarball good.tar.gz)" +# Releases stage on disk, so a large one does not count against the memory limit. +workdir_under() { + local dir rc=0 + dir="$(TMPDIR="$1" _forge_workdir)" || rc=$? + echo "$rc ${dir%/*}" + rm -rf -- "$dir" +} +mkdir "$TEST_DIR/staging" +check "workdir: TMPDIR wins" "0 $TEST_DIR/staging" "$(workdir_under "$TEST_DIR/staging")" +check "workdir: an unusable TMPDIR falls back to /tmp" "0 /tmp" "$(workdir_under "$TEST_DIR/missing")" +unset TMPDIR +default_workdir="$(_forge_workdir)" +check "workdir: defaults to /var/tmp, not /tmp" "/var/tmp" "${default_workdir%/*}" +rm -rf -- "$default_workdir" +check "no helper stages in the mktemp default" "0" "$(awk '/^(fetch_and_deploy_[a-z_]+|_forge_[a-z_]+)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func" | grep -c '_tmp="\{0,1\}$(mktemp -d)\|tmpdir="\{0,1\}$(mktemp -d)')" + echo "${passed} passed, ${failed} failed" ((failed == 0)) diff --git a/tests/release-stamp-pending.sh b/tests/release-stamp-pending.sh index 0f119ea..a2643d7 100644 --- a/tests/release-stamp-pending.sh +++ b/tests/release-stamp-pending.sh @@ -82,7 +82,7 @@ rm -f "$HOME/.npm" expect "a fresh install has no previous version" "" "$(_forge_previous_version "$HOME/.npm")" # fetch_and_deploy_release: a redo deploys again, the failed run may have patched the files -eval "$(awk '/^fetch_and_deploy_release[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^(fetch_and_deploy_release|_forge_workdir)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" _forge_release_json() { printf '%s' '{"tag_name":"v2.16.0"}' >"$5"; } _forge_source_url() { echo "https://example.invalid/src.tar.gz"; } _download_source_tarball() { :; }