Harden runtime timeouts and network egress; add platform CI#5
Open
zac wants to merge 8 commits into
Open
Conversation
JavaScriptCore drains the entire promise graph inside a single
evaluateScript call because bridge calls are synchronous, so the
wall-clock poll loop after evaluation could never interrupt CPU-bound
scripts: while(true){} pinned a worker thread forever and timeoutMs
was never enforced against running JavaScript.
Install a JSContextGroupSetExecutionTimeLimit watchdog per execution
whose callback re-checks the deadline and the cancellation flag every
50ms and terminates the script when either trips. Termination is
classified as EXECUTION_TIMEOUT (or SEARCH_TIMEOUT for search) and
CANCELLED, so call.cancel() now interrupts in-flight JavaScript
instead of only flipping a flag that was polled after completion.
The deadline now starts when evaluation begins rather than after the
script settles, and the dangling-promise poll loop reuses the same
deadline instead of granting a second full timeout budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
network.fetch previously accepted any HTTP(S) destination once the capability was allowlisted, so agent-authored JavaScript could reach loopback services, RFC 1918 hosts, and cloud metadata endpoints such as 169.254.169.254, follow redirects from public URLs to internal ones, and buffer arbitrarily large response bodies into memory. Introduce NetworkAccessPolicy, injected via CodeModeConfiguration.networkAccessPolicy: - .standard (the default) refuses loopback, private-range, link-local, CGNAT, and unique-local IPv4/IPv6 destinations (including encoded literals like 2130706433 and 0x7f000001 via inet_aton, and IPv4-mapped IPv6), plus localhost and .local/.localhost/.internal names, and caps buffered responses at 10 MB. - allowedHosts/blockedHosts support exact and subdomain matching, with explicit allowlist entries able to deliberately re-enable private hosts. .permissive restores the previous unrestricted behavior. - The fetch transfer now runs through a per-task URLSession delegate so redirect targets are re-validated before being followed and oversized bodies are cancelled mid-transfer instead of buffered. - Refusals throw structured NETWORK_POLICY_VIOLATION errors with repair suggestions, and both denials and successful fetch destinations are written to the audit logger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
The install snippet told users to depend on from: "0.1.0", but the repository has no tags, so Swift Package Manager cannot resolve that requirement. Document the branch-based dependency as the working form until a release is tagged, and keep the versioned form as the preferred path once 0.1.0 exists. Also drop the deprecated name: parameter from the .package declaration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Compile blocker: JSContextGroupSetExecutionTimeLimit and
JSContextGroupClearExecutionTimeLimit are exported by JavaScriptCore
but declared only in a private WebKit header, so the watchdog would
not compile against the public SDK module. Route both through a new
CCodeModeJSC C shim target that re-declares the exported symbols, and
document the private-API dependency in the watchdog and README.
Correctness (execution):
- The post-evaluation poll loop used 'while Date() < deadline' as its
guard, so a script that had already fulfilled just as the deadline
passed skipped the loop entirely and threw a spurious timeout that
discarded a completed result. Replace both poll loops with a shared
waitForSettlement helper that checks the settled state before the
deadline on every iteration (and re-checks watchdog termination),
and returns an already-settled result even a hair past the deadline.
- Result serialization ran with the watchdog uninstalled, so a result
with a runaway getter/toJSON (e.g. { get x() { while(true){} } })
hung the execution thread forever. Keep the watchdog installed and
rearm it with a fresh bounded budget for the decode phase, so
legitimate serialization still completes while runaway getters are
terminated.
- Fold the duplicated execute/search termination + poll logic into the
shared helper.
Security (network policy):
- Host normalization now strips a trailing root dot, so fully-qualified
spellings like 'localhost.' or 'metadata.google.internal.' can no
longer bypass the loopback/metadata block or the allow/deny lists.
- IPv6 private detection now also covers IPv4-compatible (::a.b.c.d) and
NAT64 (64:ff9b::/96) embeddings of private/loopback IPv4 addresses.
Efficiency:
- Reserve the response buffer from a known Content-Length instead of
growing it through repeated reallocations, and build the success
audit/log summary string once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
swift test on macOS never compiled the UIKit presenters or the CCodeModeJSC watchdog shim against the iOS/visionOS SDKs, so a platform-specific break (like the private-header symbol the shim resolves) could land undetected. Add a platform-build matrix that runs xcodebuild for iOS and visionOS on every PR and push, plus SwiftPM build caching and toolchain-version logging on the deterministic job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
CLRegion / CLCircularRegion are unavailable on visionOS, so the region-scoped CLGeocoder.geocodeAddressString(_:in:) overload and the coreLocationRegion helper failed to compile there. The package declared .visionOS(.v2) support but was never actually built for visionOS until the new platform-build CI job exercised it. Use the unscoped geocodeAddressString overload on visionOS and compile the region helper out there; other platforms keep region-scoped geocoding. Pre-existing issue surfaced by the new CI matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the critical issues from a repo evaluation: timeouts that couldn't interrupt running JavaScript, an unrestricted
fetch(SSRF), and install docs that couldn't resolve. A high-effort self-review of the fixes then surfaced a compile blocker and several bugs, which are also fixed here.Runtime: real timeouts & cancellation
evaluateScriptcall, so the old wall-clock poll loop never ran while JS executed —while(true){}pinned a worker thread forever andtimeoutMswas never enforced. AddedExecutionWatchdog, which installs a JavaScriptCore execution time limit so CPU-bound scripts are preemptively terminated andcancel()interrupts in-flight JS.JSContextGroupSetExecutionTimeLimit/...Clear...are exported by JavaScriptCore but declared only in a private WebKit header, so they aren't visible through the public SDK module. They're reached through a new thin C shim target,CCodeModeJSC. This is JavaScriptCore's only interruption mechanism; the private-symbol dependency is documented in the README and the watchdog source so a host can make an informed App Store decision.waitForSettlementhelper), and result serialization ran with the watchdog uninstalled so a runaway getter/toJSONhung the thread forever (the watchdog now stays armed with a fresh bounded budget for the decode phase).Security: network egress policy (SSRF)
NetworkAccessPolicyonCodeModeConfiguration. The default blocks loopback, RFC 1918, link-local (including cloud metadata like169.254.169.254), CGNAT, and unique-local destinations — including alternate/encoded IPv4 literals and IPv4-in-IPv6 (IPv4-mapped, IPv4-compatible, and NAT64) forms — pluslocalhost/.local/.internalnames and fully-qualified trailing-dot spellings.allowedHosts/blockedHostsand.permissivegive hosts control. Allowed and denied egress destinations are written to the audit logger.Docs
from: "0.1.0"against a tagless repo (SPM can't resolve it); it documents branch-based install until0.1.0is tagged, and adds sections on the network access policy and the timeout/cancellation semantics.CI
platform-buildmatrix (xcodebuildfor iOS and visionOS) so the UIKit presenters and theCCodeModeJSCshim compile against those SDKs —swift teston macOS never did — plus SwiftPM build caching and toolchain logging.Tests
ExecutionWatchdogTests(infinite loops, loop-in-promise-chain, uncatchable timeout, settles-at-deadline returns result, infinite getter during serialization, explicit cancel, search timeout) andNetworkAccessPolicyTests(private/loopback/link-local/CGNAT, encoded IPv4, IPv6 incl. NAT64, trailing-dot, allow/deny lists, size cap, redirect refusal).Verification note
Authored in a Linux environment without a Swift toolchain or JavaScriptCore, so nothing here was compiled or run locally — this PR is opened specifically to let CI build and test it on macOS/iOS/visionOS. A remaining
TODO.md(committed on this branch) tracks the follow-up work the evaluation surfaced (JS heap cap, concurrency bound, HealthKit-always-denied, calendar-span validation drift, Security-layer tests, and more).🤖 Generated with Claude Code
https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Generated by Claude Code