Found during an adversarial dogfooding pass on main @ 6a40258 (v1.0.0+).
Environment
- Debian 12 (glibc 2.36), kernel 6.6.69-cube, x86_64
- Stock swift.org toolchains: reproduced on both 6.0.3 (ubuntu22.04 build) and 6.1.2 (debian12 build)
- Not reproducible on ubuntu-latest CI (linux-smoke is green on main), so this may be distro/kernel specific. Filing because it fully blocks
fledge lanes run verify for a Linux contributor in this environment.
Symptoms
aps watch counter --timeout 1 --jsonl never stops; it has to be killed. Expected: exit 124 after ~1s (Scripts/smoke.sh line 149 asserts exactly this, so smoke hangs here).
aps watch note --jsonl prints the initial value, then never polls: external writes to note.json go unreported until a signal arrives. Sending SIGINT wakes the loop, flushes one late event (intermediate values missed), then exits 130.
swift test intermittently deadlocks mid-suite (about 2 of 5 runs here, at different tests each time, including a synchronous test's async setUp). lldb on the hung bundle shows the main thread parked in XCTWaiter.wait -> RunLoop.run -> ppoll with only the dispatch manager thread alive; the async test's MainActor hop (e.g. await MainActor.run in setUp) never resumes.
Minimal repro (no aps code)
import Foundation
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
print("returned") // never prints in this environment
Control experiment: DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) DOES fire while the main thread sits inside RunLoop.run(until:), so main-queue draining works. Only the CFRunLoop limit-date timeout (and CFRunLoopTimer) wakeups never arrive.
Code reading (swift-corelibs-foundation 6.0.3, Sources/CoreFoundation/CFRunLoop.c): on Linux the wait is __CFRunLoopServiceFileDescriptors(waitSet, CFPORT_NULL, poll ? 0 : TIMEOUT_INFINITY, ...), and the limit-date timeout is armed as a dispatch timer whose handler calls CFRunLoopWakeUp. That wakeup never lands here, so the epoll wait sleeps forever.
Impact on aps
Scripts/smoke.sh hangs at the --timeout 1 assertion, so fledge lanes run verify cannot complete (verified: verify dies at the test step here, and smoke independently at the watch step).
watchBlocking / watchStatsBlocking drive their polling cadence with RunLoop.current.run(until:) slices (Sources/aps/StateStore.swift), so all time-based watch behavior is dead on this platform: no poll interval, no --timeout, signal handling only works because signal delivery itself wakes the loop.
- Async XCTest cases deadlock whenever the waiter run loop stops waking (same plumbing).
Possible directions
- Replace the polling slices with
Thread.sleep + a zero-timeout run-loop drain, or add a watchdog path for --timeout/signals that does not depend on CFRunLoop timeouts.
- Consider making the Linux test harness less dependent on async MainActor hops (the CLI's synchronous
MainActor.assumeIsolated pattern has been reliable here).
- Track upstream: this looks like a corelibs-foundation/libdispatch platform issue rather than aps logic, but aps currently has no workaround.
Happy to run further diagnostics (lldb backtraces and repro scripts preserved).
Found during an adversarial dogfooding pass on main @ 6a40258 (v1.0.0+).
Environment
fledge lanes run verifyfor a Linux contributor in this environment.Symptoms
aps watch counter --timeout 1 --jsonlnever stops; it has to be killed. Expected: exit 124 after ~1s (Scripts/smoke.sh line 149 asserts exactly this, so smoke hangs here).aps watch note --jsonlprints the initial value, then never polls: external writes tonote.jsongo unreported until a signal arrives. Sending SIGINT wakes the loop, flushes one late event (intermediate values missed), then exits 130.swift testintermittently deadlocks mid-suite (about 2 of 5 runs here, at different tests each time, including a synchronous test's asyncsetUp). lldb on the hung bundle shows the main thread parked inXCTWaiter.wait -> RunLoop.run -> ppollwith only the dispatch manager thread alive; the async test's MainActor hop (e.g.await MainActor.runinsetUp) never resumes.Minimal repro (no aps code)
Control experiment:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2)DOES fire while the main thread sits insideRunLoop.run(until:), so main-queue draining works. Only the CFRunLoop limit-date timeout (and CFRunLoopTimer) wakeups never arrive.Code reading (swift-corelibs-foundation 6.0.3,
Sources/CoreFoundation/CFRunLoop.c): on Linux the wait is__CFRunLoopServiceFileDescriptors(waitSet, CFPORT_NULL, poll ? 0 : TIMEOUT_INFINITY, ...), and the limit-date timeout is armed as a dispatch timer whose handler callsCFRunLoopWakeUp. That wakeup never lands here, so the epoll wait sleeps forever.Impact on aps
Scripts/smoke.shhangs at the--timeout 1assertion, sofledge lanes run verifycannot complete (verified: verify dies at the test step here, and smoke independently at the watch step).watchBlocking/watchStatsBlockingdrive their polling cadence withRunLoop.current.run(until:)slices (Sources/aps/StateStore.swift), so all time-based watch behavior is dead on this platform: no poll interval, no--timeout, signal handling only works because signal delivery itself wakes the loop.Possible directions
Thread.sleep+ a zero-timeout run-loop drain, or add a watchdog path for--timeout/signals that does not depend on CFRunLoop timeouts.MainActor.assumeIsolatedpattern has been reliable here).Happy to run further diagnostics (lldb backtraces and repro scripts preserved).