Category: bug Severity: minor
Location: Sources/ARCP/Runtime/JobManager.swift:346-349
Spec: n/a
What
handleCancel spawns a detached Task that sleeps for deadlineMs then calls escalateCancelIfNeeded. This task is not stored in the JobRecord, so shutdown() (which cancels runTask/heartbeatTask) cannot cancel it. If the session ends or the job completes normally before the deadline, the task survives and fires escalateCancelIfNeeded after the fact; the guard !record.state.isTerminal saves correctness, but with a long deadlineMs (client-controlled) the runtime holds the closure and a strong-ish reference for that whole duration regardless of session lifetime. A client can submit many cancels with huge deadlineMs to accumulate sleeping tasks.
Evidence
Task { [weak self, deadline = payload.deadlineMs] in
try? await Task.sleep(for: .milliseconds(deadline))
await self?.escalateCancelIfNeeded(jobId: jobId, reason: "deadline elapsed")
}
No handle retained; shutdown() never cancels it; deadline is taken straight from untrusted payload.deadlineMs with no upper bound.
Proposed fix
- Store the escalation task in the
JobRecord and cancel it in transition(to: terminalState) and shutdown().
- Clamp
payload.deadlineMs to a sane maximum (and reject negative values — Task.sleep(for: .milliseconds(negative)) is also a concern).
Acceptance criteria
Category: bug Severity: minor
Location:
Sources/ARCP/Runtime/JobManager.swift:346-349Spec: n/a
What
handleCancelspawns a detachedTaskthat sleeps fordeadlineMsthen callsescalateCancelIfNeeded. This task is not stored in theJobRecord, soshutdown()(which cancelsrunTask/heartbeatTask) cannot cancel it. If the session ends or the job completes normally before the deadline, the task survives and firesescalateCancelIfNeededafter the fact; the guard!record.state.isTerminalsaves correctness, but with a longdeadlineMs(client-controlled) the runtime holds the closure and a strong-ish reference for that whole duration regardless of session lifetime. A client can submit many cancels with hugedeadlineMsto accumulate sleeping tasks.Evidence
No handle retained;
shutdown()never cancels it;deadlineis taken straight from untrustedpayload.deadlineMswith no upper bound.Proposed fix
JobRecordand cancel it intransition(to: terminalState)andshutdown().payload.deadlineMsto a sane maximum (and reject negative values —Task.sleep(for: .milliseconds(negative))is also a concern).Acceptance criteria
deadlineMsis bounded.