Skip to content

webui scan subprocesses run with no deadline, so a hung parser holds one of four scan slots indefinitely and OPENANT_INVOKE_TIMEOUT has no effect (PR #237's stated follow-up) #320

Description

@gadievron

Summary

Scans started from the web UI run their Python subprocess with no deadline at all. The job
context is cancel-only, and the InvokeCtx / InvokeCtxCapture entry points never consult
resolveInvokeTimeout(), so OPENANT_INVOKE_TIMEOUT has no effect on this path. The CLI path is
bounded at 30 minutes by default; the server path is unbounded.

Because a running scan holds one of four server slots for its whole duration, a wedged subprocess
permanently consumes a quarter of the server's scan capacity with no automatic recovery.

This is the follow-up merged PR #237 said it was leaving for later.

The job context carries no deadline

apps/openant-cli/internal/server/server.go:655:

	ctx, cancel := context.WithCancel(context.Background())

context.WithCancel gives cancellation but no timeout. Repo-verified: server.go contains exactly
two context.WithTimeout call sites, and neither covers the Python subprocess —

1520:	rctx, cancel := context.WithTimeout(ctx, 5*time.Second)     // DNS resolution
1545:	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)     // git clone

The invoke path never applies the configured timeout

apps/openant-cli/internal/python/invoke_ctx.go contains zero occurrences of
resolveInvokeTimeout (/usr/bin/grep -c resolveInvokeTimeout0). It takes the caller's context
and hands it straight to exec.CommandContext. The CLI path does the opposite —
apps/openant-cli/internal/python/invoke.go:91:

	ctx, cancel := context.WithTimeout(context.Background(), resolveInvokeTimeout())

with defaultInvokeTimeout = 30 * time.Minute and the OPENANT_INVOKE_TIMEOUT override documented
alongside it.

Complete site enumeration (CODE-TRACED, not executed)

All three server call sites pass a deadline-free context:

server.go:1152   python.InvokeCtxCapture(job.ctx, ...)   // the scan itself
server.go:1620   python.InvokeCtxCapture(ctx, ...)       // report-data
server.go:1669   python.InvokeCtx(ctx, ...)              // summary (makes LLM calls)

I have not executed a hung scan against a live server — that needs a wedged parser and a running
openant serve. The evidence here is the call graph, and I am labelling it as such.

Operational consequence

server.go:221 sizes the scan semaphore at four:

		sem:            make(chan struct{}, 4),

and :1096-1097 holds a slot for the entire run:

	case s.sem <- struct{}{}:
		defer func() { <-s.sem }()

So an unbounded subprocess does not merely hang its own job — it removes 1 of 4 slots until someone
cancels it by hand. The CLI cannot get into this state.

Why this is not the "cancel-only by design" case

server.go:1541-1545 is worth quoting in full, because read partially it looks like a ratification
of the missing deadline and it is the opposite:

	// Bound the clone in time so a hostile remote that streams forever can't hold a
	// scan slot indefinitely (the parent ctx is cancel-only). Disk size is not
	// bounded here — a huge working tree is a documented limitation for a
	// local, user-chosen scan target.
	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)

"The parent ctx is cancel-only" is the justification for adding a local deadline, not an argument
that none is needed. The same pattern appears at :1520 for DNS. The maintainers' approach is
evidently: leave the job context cancel-only, and bound each subprocess that could run away. The
Python invoke is the one that never got that treatment.

Prior art — this is the maintainers' own stated follow-up

Merged PR #237:

that webui path does not honor OPENANT_INVOKE_TIMEOUT... unifying the timeout handling across
both is a reasonable follow-up once both are merged.

Both are merged. No issue tracks it — a search across all open and closed issues for
OPENANT_INVOKE_TIMEOUT / InvokeCtx / webui-timeout returns nothing.

Suggested fix

  1. Apply resolveInvokeTimeout() in InvokeCtx / InvokeCtxCapture, or wrap the three server call
    sites in context.WithTimeout(ctx, resolveInvokeTimeout()) — matching the local-bound pattern
    already used at :1520 and :1545.
  2. Keep the job context cancel-only. This asks for parity with the CLI's operator escape hatch, not
    for a job-level deadline that would truncate legitimately long scans; OPENANT_INVOKE_TIMEOUT
    already exists precisely so an operator can raise it.
  3. On timeout, surface a distinct status so the UI can show "timed out" rather than a generic failure.

What I am not claiming

  • I am not claiming a hang has occurred in a real deployment. The evidence is the call graph;
    the consequence is code-traced, not executed.
  • I am not asking for a deadline on the job context itself — see fix item 2.
  • I have not measured how often a Python subprocess wedges in practice.

Grounding curve (added 2026-08-22)

commit job ctx cancel-only invoke_ctx skips timeout 3 call sites sem=4 env var exists verdict
ad17f3f (PR #133) no server file absent partial (30 min fixed) FAILS
b8844d4 (PR #235) true true true true not yet PARTIAL
a601f82 (PR #237) true true true true true FULL
b5019628 (HEAD) true true true true true FULL

Attribution: every server-side leg entered in one squashed commit — PR #235 (b8844d4,
"feat(webui): add a local web UI for the scan pipeline"). git log -- internal/server/server.go
returns exactly that one commit, so there has been no drift since. A fix belongs against #235.
PR #237 (a601f82) added resolveInvokeTimeout and OPENANT_INVOKE_TIMEOUT to invoke.go only —
it never touched the server. Note the headline phrasing is only sayable from #237: before it there
was no env var to have no effect, and the honest statement would have been "the server path has no
deadline while the CLI is fixed at 30 minutes".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions