Skip to content

Commit f11183a

Browse files
leculverCopilotCopilot
authored
!DumpStackObjects now checks alternate signal stack on Linux (dotnet#5723)
When a Linux thread is on an alternate signal stack (e.g. stack overflow handler), SP points to the handler stack while managed objects live on the normal thread stack. The existing code only scans SP + 0xFFFF and misses everything. This change adds GetStackRangeWithAltStackDetection() which collects frame SPs from the managed stack walk, sorts them, and detects a disjoint gap (>4MB) indicating two separate stack regions. When found, both the handler stack and normal thread stack are scanned for objects. macOS uses Mach exceptions rather than sigaltstack, so this scenario does not happen there. No platform check is needed because the frame- based gap detection is harmless when stacks are contiguous. Fixes dotnet#4814 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 60e8d88 commit f11183a

1 file changed

Lines changed: 165 additions & 5 deletions

File tree

src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs

Lines changed: 165 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ public override void Invoke()
4141
}
4242

4343
MemoryRange range;
44-
if (Bounds is not null || Bounds.Length == 0)
44+
MemoryRange additionalRange = default;
45+
if (Bounds is null || Bounds.Length == 0)
4546
{
46-
range = GetStackRange();
47+
range = GetStackRangeWithAltStackDetection(out additionalRange);
4748
}
4849
else if (Bounds.Length == 2)
4950
{
@@ -66,7 +67,7 @@ public override void Invoke()
6667
throw new ArgumentException($"Invalid range {range.Start:x} - {range.End:x}");
6768
}
6869

69-
PrintStackObjects(range);
70+
PrintStackObjects(range, additionalRange);
7071
}
7172

7273
[HelpInvoke]
@@ -84,15 +85,37 @@ interested in objects with invalid fields.
8485
The abbreviation 'dso' can be used for brevity.
8586
";
8687

87-
private void PrintStackObjects(MemoryRange stack)
88+
private void PrintStackObjects(MemoryRange stack, MemoryRange additionalRange)
8889
{
8990
Console.WriteLine($"OS Thread Id: 0x{CurrentThread.ThreadId:x} ({CurrentThread.ThreadIndex})");
9091

92+
if (additionalRange.Start != 0 && additionalRange.End != 0)
93+
{
94+
Console.WriteLine($"Note: Thread appears to be on alternate signal stack. Also scanning normal thread stack {additionalRange.Start:x}-{additionalRange.End:x}.");
95+
}
96+
9197
Table output = new(Console, Pointer, DumpObj, TypeName);
9298
output.WriteHeader("SP/REG", "Object", "Name");
9399

94100
int regCount = ThreadService.Registers.Count();
95-
foreach ((ulong address, ClrObject obj) in EnumerateValidObjectsWithinRange(stack).OrderBy(r => r.StackAddress))
101+
IEnumerable<(ulong StackAddress, ClrObject Object)> results = EnumerateValidObjectsWithinRange(stack);
102+
103+
// If we have an additional range (e.g., normal thread stack when on alt stack),
104+
// also scan that range for managed objects.
105+
if (additionalRange.Start != 0 && additionalRange.End != 0)
106+
{
107+
results = results.Concat(EnumerateValidObjectsWithinRange(additionalRange));
108+
}
109+
110+
// Deduplicate by StackAddress to avoid duplicate register entries and
111+
// any other duplicates that may arise from scanning multiple ranges.
112+
IEnumerable<(ulong StackAddress, ClrObject Object)> orderedResults =
113+
results
114+
.GroupBy(r => r.StackAddress)
115+
.Select(g => g.First())
116+
.OrderBy(r => r.StackAddress);
117+
118+
foreach ((ulong address, ClrObject obj) in orderedResults)
96119
{
97120
Console.CancellationToken.ThrowIfCancellationRequested();
98121

@@ -368,6 +391,143 @@ private MemoryRange GetStackRange()
368391
return new(AlignDown(stackPointer), AlignUp(end));
369392
}
370393

394+
/// <summary>
395+
/// Gets the stack range for the current thread, also returning an additional
396+
/// range if the thread appears to be executing on an alternate signal stack.
397+
/// On Linux, when a signal handler runs on the alternate stack (e.g. stack
398+
/// overflow), the SP points to the handler stack while managed objects live
399+
/// on the normal thread stack. macOS uses Mach exceptions instead of
400+
/// sigaltstack so this scenario does not arise there, but no platform check
401+
/// is needed because the frame-based gap detection is harmless when there is
402+
/// no disjoint stack.
403+
/// </summary>
404+
private MemoryRange GetStackRangeWithAltStackDetection(out MemoryRange additionalRange)
405+
{
406+
additionalRange = default;
407+
408+
int spIndex = ThreadService.StackPointerIndex;
409+
if (!CurrentThread.TryGetRegisterValue(spIndex, out ulong stackPointer))
410+
{
411+
throw new DiagnosticsException($"Unable to get the stack pointer for thread {CurrentThread.ThreadId:x}.");
412+
}
413+
414+
ulong end = 0;
415+
416+
// On Windows we have the TEB to know where to end the walk.
417+
ulong teb = CurrentThread.GetThreadTeb();
418+
if (teb != 0)
419+
{
420+
// The stack base is after the first pointer, see TEB and NT_TIB.
421+
MemoryService.ReadPointer(teb + (uint)MemoryService.PointerSize, out end);
422+
}
423+
424+
if (end != 0)
425+
{
426+
return new(AlignDown(stackPointer), AlignUp(end));
427+
}
428+
429+
// TEB not available (Linux/Unix). Use managed stack frames to determine
430+
// the real stack boundaries. This is important when the thread is on an
431+
// alternate signal stack, where the SP points to the alt stack memory
432+
// but managed objects reside on the normal thread stack.
433+
ClrThread clrThread = Runtime.Threads.FirstOrDefault(t => t.OSThreadId == CurrentThread.ThreadId);
434+
if (clrThread is not null)
435+
{
436+
List<ulong> frameSPs = new(64);
437+
foreach (ClrStackFrame frame in clrThread.EnumerateStackTrace())
438+
{
439+
if (frame.StackPointer != 0)
440+
{
441+
frameSPs.Add(frame.StackPointer);
442+
}
443+
}
444+
445+
if (frameSPs.Count > 0)
446+
{
447+
frameSPs.Sort();
448+
449+
// Detect disjoint stack regions by finding large gaps between
450+
// consecutive frame SPs. This happens when a signal handler runs
451+
// on an alternate or handler stack — the managed frames span two
452+
// non-contiguous memory regions. The threshold is set to 4MB to
453+
// avoid false positives from large stackalloc or deep native call
454+
// chains between managed frames while still being far smaller than the
455+
// address-space gap between disjoint stack regions (typically in the
456+
// multi-GB range due to separate mmap regions in the virtual address space).
457+
const ulong GapThreshold = 0x400000; // 4 MB
458+
459+
ulong largestGap = 0;
460+
int gapIndex = -1;
461+
for (int i = 1; i < frameSPs.Count; i++)
462+
{
463+
ulong gap = frameSPs[i] - frameSPs[i - 1];
464+
if (gap > largestGap)
465+
{
466+
largestGap = gap;
467+
gapIndex = i;
468+
}
469+
}
470+
471+
if (largestGap > GapThreshold && gapIndex > 0)
472+
{
473+
// Frames span two disjoint memory regions.
474+
ulong lowerMin = frameSPs[0];
475+
ulong lowerMax = frameSPs[gapIndex - 1];
476+
ulong upperMin = frameSPs[gapIndex];
477+
ulong upperMax = frameSPs[frameSPs.Count - 1];
478+
479+
MemoryRange lowerRange = new(AlignDown(lowerMin), AlignUp(lowerMax + 0x2000));
480+
MemoryRange upperRange = new(AlignDown(upperMin), AlignUp(upperMax + 0x2000));
481+
482+
// Use Math.Max/Min to avoid underflow when frame addresses are near zero.
483+
if (stackPointer >= Math.Max(lowerMin, GapThreshold) - GapThreshold && stackPointer <= lowerMax + 0x2000)
484+
{
485+
lowerRange = new(AlignDown(Math.Min(stackPointer, lowerMin)), lowerRange.End);
486+
additionalRange = upperRange;
487+
return lowerRange;
488+
}
489+
else if (stackPointer >= Math.Max(upperMin, GapThreshold) - GapThreshold && stackPointer <= upperMax + 0x2000)
490+
{
491+
upperRange = new(AlignDown(Math.Min(stackPointer, upperMin)), upperRange.End);
492+
additionalRange = lowerRange;
493+
return upperRange;
494+
}
495+
else
496+
{
497+
// SP is in neither range. Scan around the SP and use
498+
// the larger frame region as the additional range.
499+
ulong lowerSize = lowerMax - lowerMin;
500+
ulong upperSize = upperMax - upperMin;
501+
additionalRange = upperSize >= lowerSize ? upperRange : lowerRange;
502+
return new(AlignDown(stackPointer), AlignUp(stackPointer + 0xFFFF));
503+
}
504+
}
505+
506+
// No large gap — all frames are on one contiguous stack.
507+
ulong minFrameSp = frameSPs[0];
508+
ulong maxFrameSp = frameSPs[frameSPs.Count - 1];
509+
ulong frameEnd = maxFrameSp + 0x2000;
510+
511+
bool spBelowFrames = stackPointer < minFrameSp;
512+
bool spAboveFrames = stackPointer > frameEnd;
513+
bool isLikelyAltStack = spAboveFrames
514+
|| (spBelowFrames && (minFrameSp - stackPointer) > GapThreshold);
515+
516+
if (isLikelyAltStack)
517+
{
518+
additionalRange = new(AlignDown(minFrameSp), AlignUp(frameEnd));
519+
return new(AlignDown(stackPointer), AlignUp(stackPointer + 0xFFFF));
520+
}
521+
522+
ulong start = Math.Min(stackPointer, minFrameSp);
523+
return new(AlignDown(start), AlignUp(frameEnd));
524+
}
525+
}
526+
527+
// Fallback: no frame info available
528+
return new(AlignDown(stackPointer), AlignUp(stackPointer + 0xFFFF));
529+
}
530+
371531
private ulong AlignDown(ulong address)
372532
{
373533
ulong mask = ~((ulong)MemoryService.PointerSize - 1);

0 commit comments

Comments
 (0)