6.0 KiB
Resource management audit
Reviewed baseline: 9545bb0 on main. This review traced subprocesses,
buffers, committed blobs, temporary storage, HTTP responses, upload workers,
and cancellation across the public facade, CLI, and internal packages. The
findings below are based on source inspection; no load measurements or new
tests were run. The existing concurrency audit records
inherited subprocess descriptors delaying cancellation and batch-reader
shutdown, and the security audit records unbounded unique
asset redirects and remote asset reads beyond the expected size. Those remain
separate remediation items.
1. Go command output is retained for the entire execution
Relevant code: gotool.execute and combinedWriter, and its callers in shared validation and dependency preparation.
Every write to the combined stdout/stderr writer is appended to an in-memory
buffer, even for status-only Run calls and even when output is forwarded to
a caller's writer. A large or long-running Go test suite, compiler, or module
download with voluminous diagnostics therefore retains memory proportional to
all output until the process exits. On failure, formatting the full output in
an error can add another large allocation. Multiple simultaneous operations
multiply that consumption and can exhaust the process or host. The
internal guide already
documents the full-capture choice and its memory tradeoff.
Potential approaches: Evaluate spooling status-only diagnostics to a
temporary file while streaming output, with explicit cleanup and error
handling, or select a bounded diagnostic policy after deciding how much error
output callers should retain. Preserve full output where Tool.Output needs
it, forwarding behavior, error identity, and useful failure diagnostics; a
truncation policy changes the current diagnostic behavior and needs a deliberate
decision. Avoid replacing memory exhaustion with unbounded, unowned disk usage.
2. Forge JSON responses have no resource bound
Relevant code: getJSON, getRelease, postRelease, and decodeJSON, including attachment listing in assets.go and upload response decoding in upload.go.
Successful HTTP response bodies are decoded directly without a byte limit. The decoder buffers complete JSON values, including fields that are not used, and attachment lists and release bodies are retained in memory. A faulty or compromised forge returning a very large value or continuously streaming an unfinished value can consume substantial memory or keep the operation active. The second decode waits for EOF to reject trailing values; a response that never ends can also prevent completion. The default client has no overall timeout, although callers can cancel through the operation context. Response bodies are closed when these functions return; the issue is consumption before they can return, not a missing close.
Potential approaches: Choose response-size and time budgets appropriate to the supported forge contract, potentially configurable for large release notes or attachment lists. Enforce a distinguishable limit rather than silently accepting truncated JSON, and retain context cancellation and malformed-response classification. For a failed create or upload response, preserve uncertain-write reconciliation: rejecting a response does not prove that the remote write failed. Any default limit is a compatibility decision, not an implied existing restriction.
3. Temporary build cleanup failures are silently discarded
Relevant code: candidate build callback,
native smoke build, and
managed delivery build. Each defers
os.RemoveAll without inspecting its error.
If removal fails, for example because permissions or filesystem availability change, the operation can return success or an unrelated failure while leaving generated binaries and their directories behind. Repeated builds can consume disk space, and an operator is not told where to clean up. The delivery guide describes managed output as removed when delivery returns. This is distinct from the previously recorded cancellation hang, which can prevent cleanup from being reached at all.
Potential approaches: Capture cleanup errors and report the affected path, joining them with an existing failure where useful. Decide how to expose a cleanup failure after remote delivery has succeeded without misrepresenting the already completed remote state or encouraging an unsafe retry. Keep caller-owned prebuilt artifacts outside this cleanup path. Apply the same review to the temporary tag-message and receipt files, which also discard removal errors.
4. A failed temporary-path resolution loses the cleanup path
Relevant code: tempdir.Outside,
at both calls to filepath.EvalSymlinks after os.MkdirTemp.
The resolved value is assigned back to directory before checking the error;
on a resolution error the cleanup uses that returned value instead of the
saved created path. If the returned path is empty or otherwise unusable,
the newly created directory is not removed. This can occur if path resolution
fails after creation, for example after a concurrent filesystem or permission
change. The helper returns an error without telling the caller the directory's
name, so the caller cannot perform cleanup. The directory is initially empty;
the impact is leaked temporary entries rather than leaked build artifacts.
Potential approach: Keep the created path until resolution succeeds and use it for error-path removal, including the fallback allocation. Preserve the original resolution error and consider reporting a cleanup error as well.