Skip to content

fix(worker): report a dynamic analysis phase timeout as a timeout - #1197

Open
Eljees wants to merge 1 commit into
ossf:mainfrom
Eljees:fix/report-dynamic-analysis-timeout-142
Open

Eljees wants to merge 1 commit into
ossf:mainfrom
Eljees:fix/report-dynamic-analysis-timeout-142

Conversation

@Eljees

@Eljees Eljees commented Sep 10, 2026

Copy link
Copy Markdown

Description

Addresses #142 — specifically the part @calebbrown described in the follow-up comment: "this does not give us any clear reporting on timeout as the cause of termination. We should record when a dynamic analysis run terminated due to a timeout."

The reporting chain for that already exists end to end, and only the source of the value is missing. On current main:

$ git grep -n 'RunStatusTimeout' -- '*.go'
internal/analysis/status.go:42:  case sandbox.RunStatusTimeout:
internal/sandbox/sandbox.go:49:  // RunStatusTimeout is used to indicate that the command failed to complete
internal/sandbox/sandbox.go:51:  RunStatusTimeout

$ git grep -n 'result.status = ' -- '*.go'
internal/sandbox/sandbox.go:507:    result.status = RunStatusSuccess
internal/sandbox/sandbox.go:509:    result.status = RunStatusFailure

StatusForRunResult maps RunStatusTimeout to StatusErrorTimeout, and internal/worker/logging.go logs that as Analysis error - timeout. Nothing ever assigns RunStatusTimeout, so that branch is unreachable and a run that hangs is reported as an ordinary analysis error.

Two changes:

1. internal/worker/rundynamic.go — give each phase a deadline.
runDynamicAnalysisPhase now runs under context.WithTimeout(..., dynamicAnalysisPhaseTimeout). The value is 25 minutes, deliberately below the sandbox container's own sleep 30m entrypoint (sandboxes/dynamicanalysis/Dockerfile), so the Go side observes the deadline rather than racing the container's forced shutdown. phaseCtx is the context already passed to dynamicanalysis.Runsb.Run, so the deadline reaches the sandbox command without any other plumbing.

2. internal/sandbox/sandbox.go — classify the result with the context in hand.
The status logic moves out of podmanSandbox.Run into classifyRunResult(ctx, err), which checks ctx.Err() == context.DeadlineExceeded before inspecting the process error. A process killed by the deadline surfaces to cmd.Wait() as a plain *exec.ExitError (signal: killed), indistinguishable from an ordinary non-zero exit, so the context has to be consulted first or the timeout is reported as RunStatusFailure. Success, ordinary failure and the unknown-error case behave exactly as before.

Verification

Go 1.26.3, in a container, against c5c4500.

Control — pristine main with only the new test file added:

$ go test ./internal/sandbox/...
# github.com/ossf/package-analysis/internal/sandbox [.../sandbox.test]
internal/sandbox/sandbox_test.go:26:20: undefined: classifyRunResult
internal/sandbox/sandbox_test.go:36:17: undefined: classifyRunResult
internal/sandbox/sandbox_test.go:53:20: undefined: classifyRunResult
FAIL    github.com/ossf/package-analysis/internal/sandbox [build failed]

With the patch:

$ go test -v -count=1 -run TestClassifyRunResult ./internal/sandbox/...
=== RUN   TestClassifyRunResult_Timeout
--- PASS: TestClassifyRunResult_Timeout (0.20s)
=== RUN   TestClassifyRunResult_Success
--- PASS: TestClassifyRunResult_Success (0.00s)
=== RUN   TestClassifyRunResult_Failure
--- PASS: TestClassifyRunResult_Failure (0.00s)
ok      github.com/ossf/package-analysis/internal/sandbox    0.209s

The timeout test does not fabricate the error: it runs sleep 5 under a 200 ms deadline and hands cmd.Run()'s actual *exec.ExitError to the classifier, which is the case that used to be misread.

go vet ./internal/sandbox/... and gofmt -l on the three touched files are both clean.

Full suite, before and after. go test ./internal/... fails in the same four packages on pristine main and with the patch — pkgmanager (TestDownload, needs network), staticanalysis, staticanalysis/basicdata and staticanalysis/parsing (all need npm, which my container lacks). The only package whose result changes is internal/sandbox, from [build failed] in the control to ok.

What I could not run here: go build ./... does not complete in my environment — google.golang.org/api fails to resolve through both the module proxy and direct mode from this network. That is unrelated to the diff (neither touched package imports it), but I would rather say so than imply a full build passed. CI will cover it.

Open question

dynamicAnalysisPhaseTimeout is a constant. If you would prefer it as a flag on the worker, or derived from the sandbox image's own timer rather than hard-coded alongside it, say the word and I will change it — I kept it constant to hold the diff to the reporting gap in the issue.


AI-assisted: I used Claude to help locate the unreachable RunStatusTimeout branch and to draft the patch and the tests. I reviewed every line, ran the control on pristine main before the fix and the suite before and after, and the analysis and the runs are mine.

The sandbox package declares RunStatusTimeout and analysis.StatusForRunResult
maps it to StatusErrorTimeout, but nothing ever assigns it, so a phase that
hangs is reported as an ordinary analysis failure.

Give each dynamic analysis phase a deadline, kept below the sandbox container's
own 30 minute self-destruct timer, and classify the run result with the context
in hand: a process killed by the deadline reaches cmd.Wait() as a plain
*exec.ExitError, so the context has to be checked before the process error.

Refs: ossf#142

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>
@Eljees
Eljees force-pushed the fix/report-dynamic-analysis-timeout-142 branch from cea60b1 to a2c947b Compare September 10, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant