Skip to content

[fix] cancel a load() thread when the run's context fires#175

Merged
vt128 merged 1 commit into
masterfrom
fix/cancel-load-threads-on-timeout
Jul 13, 2026
Merged

[fix] cancel a load() thread when the run's context fires#175
vt128 merged 1 commit into
masterfrom
fix/cancel-load-threads-on-timeout

Conversation

@vt128

@vt128 vt128 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Problem

A module executed by load() runs on its own thread. The run's timeout (RunWithTimeout / RunWithContext) cancelled only the main thread; the load thread carried the run context as a thread-local but had no cancel watcher, so a long computation inside a loaded module ran to completion and defeated the timeout — bounded only by the step budget, or unbounded when a host relies on the timeout alone (e.g. starbox RunTimeout). Since a loaded module can come from an untrusted scriptFS, this is a reachable DoS: a loaded loop outlives the deadline.

Fix

  • The load cache arms a per-load-thread context-cancel watcher for the duration of each module's execution. watchContextCancel is generalized to watchThreadCancelCtx(ctx, thread); the machine exposes watchLoadThread, which reads the load thread's run-context thread-local. Main and load threads share the one context, so a timeout cancels both; nested loads each arm and disarm their own watcher, so no goroutine leaks.
  • A load aborted by that cancellation is transient (run-scoped), not a property of the module: doLoad reports it and get() evicts the entry — otherwise a machine reused without Reset would replay the stale cancellation error on the next load of the same module. This mirrors the existing step-budget panic eviction.

Tests

TestMachine_LoadInheritsMachineContext gains two subtests (test-first, both fail before the fix): a loaded module running a long interpreter-level loop under a short RunWithTimeout returns at the deadline instead of running to completion; and a timeout-cancelled load does not poison the cache for a later reuse.

Also

Documents that MakeModuleLoaderFromString/Reader/File execute their (host-supplied) source on a bare thread and do not inherit the Machine's step budget or cancellation — a robustness caveat tracked separately for a thread-aware loader API; use the file path for a Machine-bounded module.

Verification

gofmt / go vet clean; go test -race -count=2, full ./..., and docker golang:1.19 go test -race all green. Part of the STAR-67 follow-up (residual flagged during that fix's review).

@codacy-production

codacy-production Bot commented Jul 13, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 21 complexity · 0 duplication

Metric Results
Complexity 21
Duplication 0

View in Codacy

🟢 Coverage 100.00% diff coverage · +0.02% coverage variation

Metric Results
Coverage variation +0.02% coverage variation (-1.00%)
Diff coverage 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (d97f2a1) 7874 7458 94.72%
Head commit (06b926a) 7905 (+31) 7489 (+31) 94.74% (+0.02%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#175) 35 35 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@vt128
vt128 force-pushed the fix/cancel-load-threads-on-timeout branch from 70cd8cc to fe4c003 Compare July 13, 2026 11:40
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.53%. Comparing base (d97f2a1) to head (06b926a).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #175      +/-   ##
==========================================
+ Coverage   93.52%   93.53%   +0.01%     
==========================================
  Files          50       50              
  Lines        6298     6314      +16     
==========================================
+ Hits         5890     5906      +16     
  Misses        259      259              
  Partials      149      149              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A module executed by load() runs on its own thread. The run's timeout
(RunWithTimeout / RunWithContext) cancelled only the main thread; the
load thread carried the run context as a thread-local but had no cancel
watcher, so a long computation inside a loaded module ran to completion
and defeated the timeout — bounded only by the step budget, or unbounded
when a host relies on the timeout alone (e.g. starbox RunTimeout). Since
a loaded module can come from an untrusted scriptFS, this is a reachable
DoS: a loaded loop outlives the deadline.

The load cache now arms a per-load-thread context-cancel watcher for the
duration of each module's execution (watchContextCancel is generalized to
watchThreadCancelCtx; the machine exposes watchLoadThread, which reads the
load thread's run-context thread-local). Main and load threads share the
one context, so a timeout cancels both; nested loads each arm their own
watcher and disarm when loading finishes, so no goroutine leaks.

A load aborted by that cancellation is transient (run-scoped), not a
property of the module, so doLoad now reports it and get() evicts the
entry — otherwise a machine reused without Reset would replay the stale
cancellation error on the next run's load of the same module. This
mirrors the existing step-budget panic eviction.

TestMachine_LoadInheritsMachineContext gains two subtests: a loaded
module running a long interpreter-level loop under a short RunWithTimeout
returns at the deadline instead of running to completion; and a
timeout-cancelled load does not poison the cache for a later reuse.

Also documents that MakeModuleLoaderFromString/Reader/File execute their
(host-supplied) source on a bare thread and do not inherit the Machine's
step budget or cancellation — a robustness caveat tracked separately for a
thread-aware loader API; use the file path for a Machine-bounded module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vt128
vt128 force-pushed the fix/cancel-load-threads-on-timeout branch from fe4c003 to 06b926a Compare July 13, 2026 11:49
@vt128
vt128 merged commit 9a1636a into master Jul 13, 2026
21 checks passed
@vt128
vt128 deleted the fix/cancel-load-threads-on-timeout branch July 13, 2026 11:53
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