Skip to content

fix(interp): end the run when a builtin returns after its deadline passed - #711

Draft
astuyve wants to merge 2 commits into
mainfrom
patina/deadline-read-timeout
Draft

astuyve wants to merge 2 commits into
mainfrom
patina/deadline-read-timeout

Conversation

@astuyve

@astuyve astuyve commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

What

A run that hit its deadline could report success. read installs the run's deadline as its own kernel read deadline, so it can wake and return status 142 just before the context timer sets ctx.Err(). The post-builtin check in Runner.call only looked at ctx.Err(), so the script carried on (read x; echo after printed after) and Run returned nil.

The check now also treats a passed ctx.Deadline() as expired and stops the run with context.DeadlineExceeded. It applies to every builtin, so others that race the same way are covered too. It waits up to 100ms (deadlineGrace) for the context timer to publish ctx.Err(), so callers that check the context after Run returns see it cancelled; without that, the while/until pentest and fuzz tests in interp/tests, which assert exactly that, failed.

Credit

Found by Jacob Hayes with his deterministic-simulation tool patina (finding #4: deadline-ignored oracle with µs deadlines, about 4.9k runs; racy natively, about 3 in 2,000 at GOMAXPROCS=1, deterministic under patina at a 13 µs deadline). Findings thread: https://dd.slack.com/archives/D0AC8RLB21J/p1793475631273749

Tests

TestReadHittingRunDeadlineIsNotSuccess (interp/timeout_test.go) makes the race deterministic: read x; echo after on a pipe that never sends, under a context that reports a 20ms deadline but never fires. It asserts Run returns DeadlineExceeded and after isn't printed.

  • On main it fails 5 of 5.
  • With the fix it passes 200 of 200, and 200 of 200 at GOMAXPROCS=1.

go test ./... passes except TestPSPentestLargePIDListDoesNotHang in builtins/ps, which fails the same way on clean main on this host.

This touches the same block of interp/runner_exec.go as #708 (write errors); whichever lands second gets a rebase.

🤖 Generated with Claude Code

…ssed

`read` installs the run's deadline as its own kernel read deadline, so
it can wake and return status 142 just before the context timer sets
ctx.Err(). The post-builtin check only looked at ctx.Err(), so the
script carried on and Run returned nil: a run that hit its deadline
reported success.

The check now also treats a passed ctx.Deadline() as expired and stops
the run with context.DeadlineExceeded, for every builtin. It waits up
to 100ms for the context timer to publish ctx.Err(), so callers that
check the context after Run returns see it cancelled.

Found by Jacob Hayes with patina (https://github.com/JacobHayes/patina).

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Pipelines

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3f80938 | Docs | Give us feedback!

Windows pipes have no read deadlines, so read waits on ctx.Done() there
and cannot race ahead of the context; the test context never fires Done.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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