[fix] cancel a load() thread when the run's context fires#175
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 21 |
| Duplication | 0 |
🟢 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 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.
70cd8cc to
fe4c003
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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>
fe4c003 to
06b926a
Compare
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. starboxRunTimeout). Since a loaded module can come from an untrustedscriptFS, this is a reachable DoS: a loaded loop outlives the deadline.Fix
watchContextCancelis generalized towatchThreadCancelCtx(ctx, thread); the machine exposeswatchLoadThread, 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.doLoadreports it andget()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_LoadInheritsMachineContextgains two subtests (test-first, both fail before the fix): a loaded module running a long interpreter-level loop under a shortRunWithTimeoutreturns 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/Fileexecute 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 vetclean;go test -race -count=2, full./..., anddocker golang:1.19 go test -raceall green. Part of the STAR-67 follow-up (residual flagged during that fix's review).