You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The cDAC follows Frame.Next without checking that it makes progress. A corrupted Frame chain that contains a cycle makes the stack walk loop forever, even before the first frame is yielded.
Repro
Construct a synthetic thread whose explicit Frame head is a Frame type that doesn't produce a context, with Next pointing to itself. Then call IStackWalk.CreateStackWalk(threadData) and request one frame.
On targets without an OS thread context (always on WASM; also dumps without one), GetContextFromFrames (StackWalk_1.cs) skips Frames that don't produce a context and calls FrameIterator.Next() forever. This runs eagerly inside CreateStackWalk, so a caller's frame limit can't bound it.
Target reads stop growing (16 in the reported probe) because ProcessedData.GetOrAdd returns the revisited Frame from cache.
Other loops trust Frame.Next in the same way:
the SetupContext frame scan;
FrameIterator.Next / MoveTo while walking;
the frame-address scan near GetContextFromFrames.
The existing guard against no progress only catches a Frame that repeats back-to-back during the walk.
Expected
A cycle in the Frame chain should fail the walk (StackWalkState.Error, matching native SWA_FAILED, or an exception from context derivation) instead of hanging.
A strictly-increasing-address check is not enough. Native Frame::Push (src/coreclr/vm/frames.cpp) allows Frames up to two pages out of order and exempts alternate stacks. Detecting revisited Frame addresses in FrameIterator covers every loop.
Native Frame::Push sets m_Next before publishing the Frame, so a consistent target can't form a cycle. Only corrupted or torn target memory can, which the cDAC must tolerate.
Out of scope
A long but valid chain of Frames that don't produce a context can still cost a lot of reads before the first frame: a padded-descriptor probe read about 10 MB with a one-frame limit. A cycle guard doesn't bound that. Callers need their own read budget.
Note
This issue was drafted with assistance from GitHub Copilot.
The cDAC follows
Frame.Nextwithout checking that it makes progress. A corrupted Frame chain that contains a cycle makes the stack walk loop forever, even before the first frame is yielded.Repro
Construct a synthetic thread whose explicit Frame head is a Frame type that doesn't produce a context, with
Nextpointing to itself. Then callIStackWalk.CreateStackWalk(threadData)and request one frame.GetContextFromFrames(StackWalk_1.cs) skips Frames that don't produce a context and callsFrameIterator.Next()forever. This runs eagerly insideCreateStackWalk, so a caller's frame limit can't bound it.ProcessedData.GetOrAddreturns the revisited Frame from cache.Other loops trust
Frame.Nextin the same way:SetupContextframe scan;FrameIterator.Next/MoveTowhile walking;GetContextFromFrames.The existing guard against no progress only catches a Frame that repeats back-to-back during the walk.
Expected
A cycle in the Frame chain should fail the walk (
StackWalkState.Error, matching nativeSWA_FAILED, or an exception from context derivation) instead of hanging.A strictly-increasing-address check is not enough. Native
Frame::Push(src/coreclr/vm/frames.cpp) allows Frames up to two pages out of order and exempts alternate stacks. Detecting revisited Frame addresses inFrameIteratorcovers every loop.Native
Frame::Pushsetsm_Nextbefore publishing the Frame, so a consistent target can't form a cycle. Only corrupted or torn target memory can, which the cDAC must tolerate.Out of scope
A long but valid chain of Frames that don't produce a context can still cost a lot of reads before the first frame: a padded-descriptor probe read about 10 MB with a one-frame limit. A cycle guard doesn't bound that. Callers need their own read budget.
Note
This issue was drafted with assistance from GitHub Copilot.