Skip to content

Main thread permanently blocks in CocoaURLStreamProvider teardown path (AIR_PlayerURLAdapter clearAdapter), freezing the entire application #4322

Description

@jonathanhart

SUMMARY

On iOS (physical device, iPadOS 26.5), the AIR runtime's main thread intermittently
enters a permanently blocked state during normal application execution, while the
runtime's own internal DoActionsManager worker thread continues running. Once
blocked, NO further ActionScript executes: no ENTER_FRAME ticks, no flash.utils.Timer
callbacks (even ones with zero external dependencies), no URLLoader completion/error
events — nothing. The application appears completely frozen (a static white or blank
screen) and never recovers on its own. This happens non-deterministically: the same
binary, launched repeatedly against the same server/network conditions, sometimes
boots cleanly and sometimes freezes at this point, with no code-level trigger we've
been able to identify on the application side.

We captured this live with Xcode Instruments (Time Profiler, attached to the running
process during an active freeze) and obtained a fully symbolicated stack showing the
main thread blocked inside AIR's own native networking cleanup path.

ENVIRONMENT

  • AIR SDK: 51.3.1.1 (HARMAN-maintained build)
    (Note: 51.3.4.2 was evaluated and rejected for this project due to an unrelated
    Keychain-signing regression on macOS — not itself the subject of this report.)
  • Target: iOS, packaged via adt -package -target ipa-debug
  • Test device: iPad (A16), iPad15,7, iPadOS 26.5 (build 23F77), arm64e
  • Host: macOS, Xcode 16.0 (17F113), xctrace 16.0
  • App descriptor: standard, no block (that produces an
    unrelated packaging error — "error 105: invalid value" — on any non-empty content,
    worked around at the Info.plist level post-build, not part of this report)
  • Networking: standard flash.net.URLLoader / URLRequest, wrapped in an in-house
    HTTPRequest class with its own retry/timeout logic layered on top of URLLoader's
    own COMPLETE/IO_ERROR/HTTP_STATUS/SECURITY_ERROR events
  • Reproduces both on fresh installs and warm relaunches of an already-installed build
  • Reproduces after a full device reboot (rules out accumulated device-side memory
    pressure or leaked OS-level resources from repeated test cycles as the sole cause)

REPRODUCTION / FREQUENCY

Not consistently reproducible on demand. Across roughly 15-20 launches during one
extended debugging session, the freeze occurred at unpredictable points during
early application bootstrap — sometimes within the first ~1 second of the main
thread's own life, sometimes several seconds in, sometimes not at all (the app
boots and runs normally). No single code path, screen, or network endpoint
correlates with every occurrence. The one constant is the shape of the failure
once it happens: main thread stops being scheduled entirely; a specific AIR
runtime worker thread keeps running.

DIAGNOSTIC METHOD

  1. Launched the app fresh via xcrun devicectl device process launch.
  2. Attached Instruments' Time Profiler via xcrun xctrace record --attach <pid> --template "Time Profiler" --time-limit 60s while the app was visibly frozen
    on a static white screen (confirmed by direct visual inspection of the device,
    not inferred).
  3. Exported the raw time-sample table and the aggregated, symbolicated
    time-profile table via xcrun xctrace export.
  4. Cross-referenced against our own application-level diagnostics (a lightweight
    fire-and-forget HTTP ping emitted at every major bootstrap milestone), which
    independently confirmed zero further ActionScript execution of any kind for
    the full 60-second recording window once the freeze began — including a
    plain flash.utils.Timer we added specifically as an event-independent
    watchdog, which never fired despite having no dependency on network state.

KEY FINDING #1 — Main Thread's last-known stack, state "Blocked"

Across the entire 60-second recording, "Main Thread" (0x890a, tid 35082) appears
exactly ONCE in the raw time-sample table — at t=00:00.329 — in thread-state
"Blocked". It never appears again in that table for the remainder of the
recording, and it never appears at all in the CPU time-profile table (which only
records on-core "Running" samples) — i.e., the main thread was never observed
running again after this single sample.

We symbolicated that stack against the app's own binary (statically-linked AIR
runtime symbols are present and unstripped in the debug build) using atos:

main
-[_CTPlayerTimerGlue _doPlay:]
CorePlayer::DoPlay(bool, bool)
CorePlayer::DoPlay_EnterFrameStart()
ASyncManager::EmptyTrash(CorePlayer*)
CocoaURLStreamProvider::~CocoaURLStreamProvider()
CocoaURLStreamProvider::~CocoaURLStreamProvider()
-[AIR_PlayerURLAdapter clearAdapter]

In plain terms: the main thread's own ENTER_FRAME tick (DoPlay_EnterFrameStart)
triggered ASyncManager::EmptyTrash, AIR's routine cleanup of discarded/completed
async objects. That cleanup was in the process of destructing a
CocoaURLStreamProvider (the native iOS backend behind flash.net.URLLoader /
URLStream), and that destructor's call into -[AIR_PlayerURLAdapter clearAdapter]
is where the thread stopped and never resumed.

We have the raw, unsymbolicated frame addresses for this exact sample available on
request (or in the attached trace) in case HARMAN's internal symbol maps resolve
additional inlined frames our local atos pass could not.

KEY FINDING #2 — DoActionsManager keeps running throughout the freeze

While the main thread is blocked, a separate thread named "Idle Worship" (tid
35139) — resolving to AIR's own DoActionsManager — continues cycling
throughout the ENTIRE freeze window, roughly every 100-800ms, alternating
between these symbolicated states:

DoActionsManager::ThreadBody()
DoActionsManager::GetActionsThreadMessage(ActionsThreadMessage*)
TThreadWait::DoSleep(int)
TSafeLock::~TSafeLock()
TSafeThread::RunHelper() / TSafeThread::Run(EThreadType)
PlatformCriticalSection::PlatformEnter()
CoreCriticalSectionBase::Enter() / RemoveFromAbortList()
MMgc::EnterFrame::AddAbortUnwindObject(MMgc::AbortUnwindObject*)

This thread is alive, taking and releasing its own critical sections, and
touching the MMgc garbage collector's EnterFrame bookkeeping — but this activity
never unblocks the main thread. Whatever lock or condition the main thread is
waiting on inside the CocoaURLStreamProvider/AIR_PlayerURLAdapter teardown path
is apparently never satisfied by this thread's own progress.

WHAT WE RULED OUT

  • Not a server-side issue: Django access logs confirm the backend received and
    correctly responded to every in-flight request at the time of each freeze
    (including one case where a request that our own client-side watchdog reported
    as "pending" for 170+ seconds had, in fact, already been answered by the
    server in milliseconds).
  • Not the application's own retry/error-handling logic failing: our HTTPRequest
    wrapper has correct, tested retry/backoff/fatal-alert logic gated on URLLoader's
    COMPLETE/IO_ERROR/HTTP_STATUS/SECURITY_ERROR events — but none of those events,
    nor a plain independent Timer, ever fire once the freeze begins, because the
    main thread that would dispatch them is not running at all.
  • Not app backgrounding / screen lock: visually confirmed the device screen was
    on, unlocked, and the app was the active foreground app at the moment of at
    least one captured freeze.
  • Not a one-time device state issue: reproduced again after a full physical
    device reboot.
  • Not a deliberate application-level "give up" / kill-switch: audited the
    application's own code for anything that would intentionally stop the frame
    loop, zero the stage frame rate, or disable all timers as a group; found
    nothing that does this.

SUSPECTED ROOT CAUSE

A race or deadlock inside the AIR iOS runtime's native networking teardown path,
specifically between:

  • ASyncManager::EmptyTrash destructing a CocoaURLStreamProvider on the main
    thread during a normal ENTER_FRAME tick, and
  • whatever -[AIR_PlayerURLAdapter clearAdapter] needs to acquire or wait on
    to complete (a lock also touched by DoActionsManager's own critical-section
    cycling is one plausible candidate, given DoActionsManager's continuous
    Enter()/RemoveFromAbortList() activity throughout the freeze).

This would explain the non-determinism cleanly: it only manifests when the timing
of a URLLoader object's native-side teardown collides with this other thread's
lock cycling in just the wrong way — consistent with what we observed (heavy,
parallel network activity during app bootstrap; freeze incidence that varies
run-to-run with no single reproducible trigger).

IMPACT

This is a hard, unrecoverable freeze from the end user's perspective — no error,
no alert, no crash log, no console output reaches the device's visible UI. The
process remains alive (confirmed via devicectl device info processes) but is
permanently unresponsive. The only recovery is force-quitting and relaunching,
which may or may not hit the same race again.

WHAT WE CAN PROVIDE ON REQUEST

  • The full .trace bundle from this capture (includes symbol stores, raw
    time-sample table, and the aggregated time-profile table)
  • The exact decimal/hex frame addresses for the Main Thread's blocked sample
  • Our own application-level bootstrap diagnostics log correlated to the same
    time window
  • A minimal reproduction case, if useful — we can likely narrow this to "make N
    concurrent URLLoader requests during early boot, then let them complete/be
    discarded" if that would help HARMAN reproduce independently

Happy to answer follow-up questions or provide additional trace captures.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions