Skip to content

[iOS][interp] SIGSEGV (null deref) in StackFrameIterator::CheckForSkippedFrames during debugger stack walk of CoreCLR interpreter frames #129879

Description

@steveisok

Description

Debugging a net11.0-ios app on the iOS simulator with the CoreCLR interpreter, the target process crashes when the debugger walks a managed thread's stack — i.e., when stopping at a breakpoint and then stepping (or otherwise retrieving active frames). The fault is a null-pointer dereference in the DAC stack-frame iterator.

Stepping is what reliably triggers it; the debugger requests the call stack via ICorDebug (CordbThread::GetActiveFunctionsShimStackWalk), which calls into the DAC and segfaults.

Faulting thread (EXC_BAD_ACCESS / SIGSEGV, KERN_INVALID_ADDRESS at 0x0)

libmscordaccore.dylib  StackFrameIterator::CheckForSkippedFrames()
libmscordaccore.dylib  StackFrameIterator::ResetRegDisp(REGDISPLAY*, bool)
libmscordaccore.dylib  DacDbiInterfaceImpl::SetStackWalkCurrentContext(VMPTR_Base<Thread, __DPtr<Thread>>, void**, CorDebugSetContextFlag, DT_CONTEXT*)
libmscordbi.dylib      CordbStackWalk::SetContextWorker(CorDebugSetContextFlag, unsigned int, unsigned char*)
libmscordbi.dylib      CordbStackWalk::SetContext(CorDebugSetContextFlag, unsigned int, unsigned char*)
libmscordbi.dylib      ShimStackWalk::CheckInternalFrame(ICorDebugFrame*, ShimStackWalk::StackWalkInfo*, ICorDebugThread3*, ICorDebugStackWalk*)
libmscordbi.dylib      ShimStackWalk::Populate()
libmscordbi.dylib      ShimStackWalk::ShimStackWalk(ShimProcess*, ICorDebugThread*)
libmscordbi.dylib      ShimProcess::LookupOrCreateShimStackWalk(ICorDebugThread*)
libmscordbi.dylib      CordbThread::GetActiveFunctions(unsigned int, unsigned int*, _COR_ACTIVE_FUNCTION*)
                       (invoked by the debugger via the ICorDebug API)

The thread being walked is executing under the CoreCLR interpreter; at the time of a single-step its stack looks like:

libcoreclr.dylib  DebuggerController::DispatchPatchOrSingleStep(Thread*, _CONTEXT*, unsigned char const*, SCAN_TRIGGER)
libcoreclr.dylib  DebuggerController::DispatchNativeException(_EXCEPTION_RECORD*, _CONTEXT*, unsigned int, Thread*)
libcoreclr.dylib  Debugger::FirstChanceNativeException(_EXCEPTION_RECORD*, _CONTEXT*, unsigned int, Thread*, int)
libcoreclr.dylib  InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, ExceptionClauseArgs*)
libcoreclr.dylib  ExecuteInterpretedMethod
libcoreclr.dylib  InterpreterStubRetI8

StackFrameIterator::CheckForSkippedFrames() appears not to handle interpreter frames (InterpreterFrame / InterpMethodContextFrame) and dereferences null while resetting the register display for the active context.

Secondary issue — crash reporting masks the fault

With crash reporting enabled (default), the above SIGSEGV is not what's observed. CoreCLR's in-proc crash reporter runs from the signal handler and tries to suspend the runtime to enumerate threads, but deadlocks acquiring the ThreadStore lock (the faulting/stepping thread is hijacked in RareDisablePreemptiveGCWaitSuspendEventsHelper):

libcoreclr.dylib  CrstBase::Enter()
libcoreclr.dylib  ThreadSuspend::LockThreadStore(ThreadSuspend::SUSPEND_REASON)
libcoreclr.dylib  ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_REASON)
libcoreclr.dylib  CrashReportEnumerateThreads(...)
libcoreclr.dylib  InProcCrashReporter::EmitThreads(InProcCrashReportCrashKind, void*)
libcoreclr.dylib  InProcCrashReporter::CreateReport(int, void*)
libcoreclr.dylib  InProcCrashReportSignalDispatcher(int, void*, void*)

The crash report never completes, so CrashReportWatchdog::Abort() fires and the process exits with SIGABRT (Abort trap: 6) after the watchdog timeout. Setting DOTNET_EnableCrashReport=0 / DOTNET_DbgEnableMiniDump=0 removes the masking and surfaces the underlying SIGSEGV immediately (faulting stack above).

So there are arguably two problems:

  1. Primary: null deref in StackFrameIterator::CheckForSkippedFrames during a debugger stack walk over CoreCLR interpreter frames.
  2. Secondary: the in-proc crash reporter can deadlock in SuspendEE when the crash originates from a thread already hijacked for debugging, leaving CrashReportWatchdog to abort.

Steps to reproduce

  1. Create/run a net11.0-ios app configured to use the CoreCLR interpreter on the iOS simulator.
  2. Debug it (Pair-to-Mac), hit a breakpoint in a button handler.
  3. Step (Step Over / Into).
  4. The target crashes — SIGABRT via the crash-report watchdog by default, or SIGSEGV (null deref, stack above) with crash reporting disabled.

Notes / observations

  • Hitting/landing on a breakpoint works; stepping (which forces the active-frame stack walk mid-interpreter-execution) is the reliable trigger.
  • Disabling implicit property/func evaluation in the debugger does not help — pure single-step (DispatchPatchOrSingleStep) crashes the same way.

Environment

  • App: net11.0-ios, iossimulator-arm64, CoreCLR + interpreter
  • Simulator: iPhone 16 Pro, iOS 18.5; host macOS 26.5.1 (25F80)
  • iOS SDK: Microsoft.iOS.Sdk.net11.0_26.5 26.5.11546-net11-p5
  • Debugger: Visual Studio 18 (Canary) + Pair-to-Mac, remote CoreCLR debugging

Crash reports (.ips) captured for all three signatures (2× watchdog SIGABRT, 1× real SIGSEGV with crash reporting disabled) — available on request.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions