fix: [sc-106111] Sweep downloaded installer binaries so temp storage stops accumulating dead agents - #102
Merged
mlataza merged 2 commits intoAug 13, 2026
Conversation
…lling
Every successful auto-update leaked its installer. Download creates the file
with os.CreateTemp("", "installer-*.bin") and its deferred cleanup removes it
only on the failure path -- success = true deliberately skips the removal so the
installer can be executed -- and nothing removes it afterwards: Update spawns it
detached and returns, and no sweep covered installer-*.bin. This is the same
accumulating-temp-file leak SweepStaleScripts fixed for exec-*.ps1 in sc-103967;
the installer files were not covered.
Each leaked file is a full agent binary, so a long-lived endpoint quietly
accumulates hundreds of megabytes. On the space-constrained systems where that
matters most -- thin VDI images, small VM system disks, appliances -- a full
temp volume is not just an agent problem: it breaks Windows Installer,
application logging, and anything else needing scratch space, and the agent's
own next update then fails because os.CreateTemp cannot allocate.
The download path cannot clean up after itself, because the process that would
delete the file is the one the installer replaces. So the fix is a startup
sweep, plus a move that makes the sweep safe:
Downloads now land in <data directory>/updates, created 0700, instead of the
shared system temp directory. The sweep only ever runs against a directory this
agent created, a full agent binary is no longer left executable and
world-readable, and endpoints that mount /tmp noexec can execute the installer
at all. Uninstall already removes the data directory wholesale, so nothing is
left behind by the move.
SweepStaleInstallers runs once per process at service startup, after the service
reports itself running, and removes installer binaries older than 24 hours. A
successful update restarts the agent, so each start reclaims the previous
update's installer while leaving the current one -- far younger than the
threshold -- alone; steady state is a single file rather than one per update.
The legacy shared temp directory is swept too, so an upgraded endpoint reclaims
what it has accumulated since it was installed rather than only stopping the
growth from here on.
The matcher is the conservative one SweepStaleScripts established: regular files
only (never symlinks or device nodes), the exact installer-<digits>.bin shape
os.CreateTemp produces, and nothing under the age threshold -- which is what
makes it safe to point at a directory shared with the rest of the system. The
os.CreateTemp pattern is a shared constant used by both the download and the
sweep, so the two cannot drift. Every failure is logged and skipped; the sweep
never returns an error, because housekeeping must not block the agent from
starting.
Exercise the startup sweep end to end on all three platforms: plant an aged installer binary (plus a fresh one and a similarly named file the agent never creates) in the org's updates directory, restart, and assert only the agent's own stale download is reclaimed and the removal is logged. The fixture is planted rather than produced by a real update cycle: the sweep is what this ticket changed, and where the download lands is covered by unit tests that assert both halves share one pattern constant. Ageing is the only simulated part, so the production 24h threshold stays in play and no test-only override is needed in the agent. The files are megabytes rather than bytes so the reclaimed space shows in the directory listing the fixture prints. Only the org's own updates directory is asserted here. The sweep also covers the legacy shared temp directory older agents downloaded into, but that is the service account's temp directory -- notably root's private TMPDIR on macOS, not the runner's -- so a fixture planted from the runner would land somewhere the service never reads. That half is covered by TestExecute_SweepsStaleInstallerFilesOnStartup, which runs Execute in-process and can see its own temp directory. New action, installer-sweep-fixture: seed/assert the file fixture elevated, since the updates directory lives under the installation's data directory and belongs to the service account.
mlataza
deleted the
feature/sc-106111/sweep-downloaded-installer-binaries-so-temp
branch
August 13, 2026 15:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every successful auto-update leaves its downloaded installer binary in the system temp directory forever.
Downloadcreates the file withos.CreateTemp("", "installer-*.bin")and its deferred cleanup removes it only on the failure path —success = truedeliberately skips the removal so the installer can be executed. Nothing removes it afterwards:Updatespawns it detached and returns, and no sweep coveredinstaller-*.bin. This is the same accumulating-temp-file leakSweepStaleScriptsfixed forexec-*.ps1in sc-103967; the installer files were not covered.Each leaked file is a full agent binary, so a long-lived endpoint quietly accumulates hundreds of megabytes. On the space-constrained systems where that matters most — thin VDI images, small VM system disks, appliances — a full temp volume is not just an agent problem: it breaks Windows Installer, application logging, and anything else needing scratch space, and the agent's own next update then fails because
os.CreateTempcannot allocate. Secondary concern: these are executable files sitting0755in a world-readable directory indefinitely.The download path cannot clean up after itself, because the process that would delete the file is the one the installer replaces. So the fix is a startup sweep, plus a move that makes the sweep safe.
Changes
Downloads land in a directory the agent owns (c305c83). Installers now go to
<data directory>/updates—C:\ProgramData\RewstRemoteAgent\<orgId>\updates,/etc/rewst_remote_agent/<orgId>/updates,/Library/Application Support/rewst_remote_agent/<orgId>/updates— created0700, instead of the shared system temp directory. This buys three things: the sweep only ever runs against a directory this agent created, a full agent binary is no longer left executable and world-readable, and endpoints that mount/tmpnoexec(a common hardening baseline) can execute the installer at all. Uninstall alreadyRemoveAlls the data directory, so nothing is left behind by the move and no uninstall change is needed. The ticket asked for this to be considered explicitly; the shared temp directory was not retained.SweepStaleInstallersat service startup (c305c83). Runs once per process, after the service reports itself running (same placement and best-effort contract as the script sweep), removing installer binaries older than 24 hours. Because a successful update restarts the agent, each start reclaims the previous update's installer while leaving the current one — far younger than the threshold — alone: steady state is a single file rather than one per update. The legacy shared temp directory is swept as well, so an upgraded endpoint reclaims what it has accumulated since it was installed rather than only stopping the growth from here on.The matcher is the conservative one
SweepStaleScriptsestablished, which is what makes it safe to point at a directory shared with the rest of the system: regular files only (never symlinks or device nodes), the exactinstaller-<digits>.binshapeos.CreateTempproduces, and nothing under the age threshold. Theos.CreateTemppattern is a shared constant used by both the download and the sweep, so the two cannot drift. Every failure — unreadable directory, unremovable file (a running Windows installer holds its own image open) — is logged and skipped; the sweep never returns an error. A non-zero number of removals logs atInfowith the count and directory; individual removals log atDebug.Integration coverage (f097a1d). A scenario on all three platforms plants an aged installer, a fresh one, and a similarly named file the agent never creates in the org's updates directory, restarts, and asserts only the agent's own stale download is reclaimed. New
installer-sweep-fixtureaction seeds/asserts elevated, since the updates directory belongs to the service account.Verification
Probed live against the shipped binary, not a test re-run. Built
cmd/agent_smith, planted a 12 MiB fixture in the real shared temp directory (one aged agent-created installer, one fresh, one aged non-agent file), and ran the binary in service mode:4 MiB reclaimed; the fresh installer and the non-agent file both survived; startup was neither delayed nor failed; the nonexistent updates directory logged nothing at all (a missing directory is the fresh-install case, not an error).
Second live probe, same binary rebuilt with
-X ...updateIntervalStr=3sand auto-updates on, confirming the download target moved (run unprivileged, so themkdirunder the root-owned data directory is expected to fail — the point is which path it names):Incidental corroboration of the leak: the developer machine this was built on had six orphaned
installer-*.binfiles in its temp directory from prior runs of the pre-fix test suite, mode0755, never cleaned by anything.Falsification of the new assertion. Mutated
service.goin a disposable detached worktree to drop the legacy-temp sweep call;TestExecute_SweepsStaleInstallerFilesOnStartupwent red on both the surviving-file assertion and the missing log line. Restored, green. The author worktree was never mutated.Unit tests cover every case the ticket asked for: a stale installer is removed; a fresh one is not; files matching the prefix but not the full pattern are left alone (non-numeric middle, empty middle, wrong suffix, wrong prefix, a matching directory); a symlink is not followed (asserted with a threshold below the link's own age, plus a control file proving the sweep really was active — otherwise the assertion would pass on the age check and prove nothing); a missing directory is a silent no-op; a
ReadDirfailure is logged not fatal; a non-positivemaxAgefalls back to the default; and an unremovable file is logged and skipped without failing the sweep (Unix, via a read-only parent, skipped as root). Plus round-trip tests that a realos.CreateTempname matches the sweep's matcher, that a real download lands in the swept directory under a swept name, and that the updates directory is a subdirectory of the data directory and is not the shared temp directory.go test ./...,go vet ./...,golangci-lint run ./...all clean;GOOS=windowsandGOOS=linuxcross-builds clean.Not probed: the integration workflow scenario itself. It needs GitHub-hosted runners plus the org's
IT_*vars and secrets, so it cannot be exercised locally — it wants a CI run on this PR before merge. It is an assertion scenario, not an enforcement gate: it can produce a false green (if the fixture or restart step silently no-ops) but cannot let a violation through elsewhere. The behaviour it asserts is the same behaviour the live probe above demonstrated in the real binary, one directory over.Probe Command
Expect:
installer-900000001.bingone, the other two present, oneSwept stale installer files ... removed=1line.Notes
Failing to create the updates directory now fails the update, where previously the download would have gone to temp. This adds no new failure mode on a read-only data directory: the update flow already rewrites
config.jsonand the agent already writes its log there, so such an endpoint could not update before this change either.