Repository navigation
Description
mcpx(daemon): long-poll waitMs is uncapped and overflows — a client can pin a handler past the operator's wait settings, and a ~317-year wait returns instantly
Summary
Both mcpx daemon long-poll handlers take the client's waitMs query parameter verbatim, with no upper bound, and convert it with time.Duration(ms) * time.Millisecond, which overflows for large values:
pkgs/mcpx/internal/daemon/ops.go:476-480—handleTaskResult(GET /v1/tasks/{id}/result): the operator'stasks.resultWaitsetting (default2m,pkgs/mcpx/internal/defaults/defaults.json:83; documented indocs/configuration.mdas "how long collecting a task result blocks before giving up") is replaced by any positivewaitMsthe client sends.pkgs/mcpx/internal/daemon/routes_proto.go:514-518—handleAskPoll(GET /v1/ask/{id}): the same pattern replaces theproto.askPolldefault (500ms).
The daemon's HTTP server sets only ReadHeaderTimeout (pkgs/mcpx/internal/daemon/server.go startHTTP), so nothing else bounds how long these handlers hold a goroutine and a connection.
Verified behavior (laptop probe, this run)
Probe: ~/git/fleet/probe-2026-10-29-laneA-waitms/ (laptop worktree at main 17819113; both handler lines verified byte-identical at current main 24901e7b). The probe drives the real handleTaskResult / handleAskPoll with a never-finishing task:
| Request | Result |
|---|---|
task result waitMs=300 |
408 after 300.2ms (control: small waits are honoured) |
task result waitMs=3600000 (1h = 30× the operator's 2m default) |
still blocked after 2s; returned 200 only when the task was released — accepted, no cap |
task result waitMs=10000000000000 (1e13 ms ≈ 317 years) |
408 in 13µs — time.Duration(ms) * time.Millisecond overflows int64 (threshold ≈ 9.22e12 ms) and wraps negative, so context.WithTimeout is already expired |
ask poll waitMs=300 |
200 after 300.6ms (control) |
ask poll waitMs=10000000000000 |
200 in 19.6µs — same overflow, instant return, no error |
Legitimate in-tree clients only ever pass small waits (1ms polls in internal/mcpserver/ask.go, the 250ms TaskEager window, daemonAsker.Poll in internal/cli/serve_ask.go:205), so a cap at (or near) the configured defaults breaks nothing in-tree.
Why it matters
- Operator settings are silently overridden.
tasks.resultWaitexists so the operator decides how long result collection may block; any local API client can currently multiply it without limit, pinning a handler goroutine + connection per request for hours. - The overflow is a silent wrong answer, not an error. A wait past ~292 years (
ms > ~9.22e12) wraps to a negative or arbitrary duration: the "wait" returns instantly (task result: a spurious 408 "has not finished yet"), indistinguishable from a genuine timeout. Nothing is logged and no 400 is returned. - The same two-line pattern in both handlers means one ruling fixes both.
Acceptance criteria
- Both handlers clamp or reject an out-of-range
waitMsunder one pinned rule (triage pins: clamp to the configured default/ceiling vs 400). The operator'stasks.resultWait/ a named ask-poll ceiling is the maximum a client can obtain. - No
waitMsvalue can produce a negative or wrappedtime.Duration: values above the overflow threshold (~9.22e12 ms) follow the same pinned rule as other out-of-range values, never an instant spurious timeout. - Regression tests at the handler level: small
waitMshonoured (as today);waitMsabove the ceiling behaves per the pinned rule;waitMs=10000000000000does not return an instant 408/200. - In-tree client behavior unchanged: 1ms–500ms polls and the
tasks.resultWaitdefault path work exactly as before.
Dedup
Searched open + closed issues for waitMs, "task result wait", "long poll wait cap": no hit covers these handlers. Related but distinct: #1679 is the /v1/events since resume cursor (different endpoint, different parameter); #1864 covers call-scoped timeouts for MCP tool calls, not these HTTP long-polls.
Activity
developing-today commentedon Oct 29, 2026
CODE-1103
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
waitMsin both mcpx daemon handlers (ops.go:478 task result, routes_proto.go:516 ask poll): pin clamp-vs-400 and the ceiling, then route the fix.14 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked
dezren39 commentedon Oct 31, 2026
Lane D resumed task 4314 and re-parked it for 20m: PR 3083 head 155d659 unchanged, Codex review completed with no findings (0 unresolved threads), CI partially green (mcpx-docs build + lint SUCCESS) but plugin tests still QUEUED and stress IN_PROGRESS on run 41208623592 — fleet CI congestion. Resume: all checks green -> merge PR 3083 -> close #2231 -> complete fixed_merged.
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done