Skip to content

Commit 1c8a1c8

Browse files
committed
BUGFIX: Do not schedule unreachable EH blocks during max stack computation (#652)
1 parent 4a14333 commit 1c8a1c8

2 files changed

Lines changed: 151 additions & 46 deletions

File tree

src/AsmResolver.DotNet/Code/Cil/CilMaxStackCalculator.cs

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Runtime.CompilerServices;
45
using AsmResolver.PE.DotNet.Cil;
56

@@ -10,20 +11,36 @@ internal readonly ref struct CilMaxStackCalculator
1011
private readonly CilMethodBody _body;
1112
private readonly Stack<StackState> _agenda;
1213
private readonly int?[] _recordedStackSizes;
14+
private readonly Dictionary<int, List<CilExceptionHandler>>? _handlers;
1315

1416
public CilMaxStackCalculator(CilMethodBody body)
1517
{
1618
_body = body ?? throw new ArgumentNullException(nameof(body));
1719

18-
if (_body.Instructions.Count > 0)
20+
if (_body.Instructions.Count <= 0)
1921
{
20-
_agenda = new Stack<StackState>();
21-
_recordedStackSizes = new int?[_body.Instructions.Count];
22+
_agenda = null!;
23+
_recordedStackSizes = null!;
2224
}
2325
else
2426
{
25-
_agenda = null!;
26-
_recordedStackSizes = null!;
27+
_agenda = new Stack<StackState>();
28+
_recordedStackSizes = new int?[_body.Instructions.Count];
29+
30+
if (body.ExceptionHandlers.Count > 0)
31+
{
32+
_handlers = new Dictionary<int, List<CilExceptionHandler>>(body.ExceptionHandlers.Count);
33+
foreach (var handler in body.ExceptionHandlers)
34+
{
35+
if (handler.TryStart is not { Offset: int startOffset })
36+
continue;
37+
38+
if (!_handlers.TryGetValue(startOffset, out var list))
39+
_handlers.Add(startOffset, list = new List<CilExceptionHandler>());
40+
41+
list.Add(handler);
42+
}
43+
}
2744
}
2845
}
2946

@@ -34,14 +51,14 @@ public int Compute()
3451

3552
int result = 0;
3653

37-
// Add entry points to agenda.
38-
ScheduleEntryPoints();
54+
// Schedule offset 0.
55+
_agenda.Push(new StackState(0, 0));
3956

4057
while (_agenda.Count > 0)
4158
{
4259
var currentState = _agenda.Pop();
4360

44-
// Check if we got passed the end of the method body. This only happens if the CIL code is invalid.
61+
// Check if we passed the end of the method body. This only happens if the CIL code is invalid.
4562
if (currentState.InstructionIndex >= _body.Instructions.Count)
4663
{
4764
var last = _body.Instructions[_body.Instructions.Count - 1];
@@ -53,15 +70,18 @@ public int Compute()
5370
{
5471
// Check if previously visited state is consistent with current observation.
5572
if (recordedStackSize.Value != currentState.StackSize)
73+
{
5674
throw new StackImbalanceException(_body, _body.Instructions[currentState.InstructionIndex].Offset);
75+
}
5776
}
5877
else
5978
{
6079
// Mark instruction as visited and store current state.
6180
_recordedStackSizes[currentState.InstructionIndex] = currentState.StackSize;
6281

6382
// Schedule successors of current instruction.
64-
ScheduleSuccessors(currentState);
83+
ScheduleNaturalSuccessors(in currentState);
84+
ScheduleExceptionalSuccessors(in currentState);
6585
}
6686

6787
// Maintain largest found stack size.
@@ -72,39 +92,7 @@ public int Compute()
7292
return result;
7393
}
7494

75-
private void ScheduleEntryPoints()
76-
{
77-
// Schedule offset 0.
78-
_agenda.Push(new StackState(0, 0));
79-
80-
// Handler blocks are not referenced explicitly by instructions.
81-
// Therefore we need to schedule them explicitly as well.
82-
var instructions = _body.Instructions;
83-
84-
for (int i = 0; i < _body.ExceptionHandlers.Count; i++)
85-
{
86-
var handler = _body.ExceptionHandlers[i];
87-
88-
// Determine stack size at the start of the handler block.
89-
int stackDelta = handler.HandlerType switch
90-
{
91-
CilExceptionHandlerType.Exception => 1,
92-
CilExceptionHandlerType.Filter => 1,
93-
CilExceptionHandlerType.Finally => 0,
94-
CilExceptionHandlerType.Fault => 0,
95-
_ => throw new ArgumentOutOfRangeException(nameof(handler.HandlerType))
96-
};
97-
98-
if (handler.TryStart is {Offset: var o1 })
99-
_agenda.Push(new StackState(instructions.GetIndexByOffset(o1), 0));
100-
if (handler.HandlerStart is {Offset: var o2 })
101-
_agenda.Push(new StackState(instructions.GetIndexByOffset(o2), stackDelta));
102-
if (handler.FilterStart is {Offset: var o3})
103-
_agenda.Push(new StackState(instructions.GetIndexByOffset(o3), 1));
104-
}
105-
}
106-
107-
private void ScheduleSuccessors(in StackState currentState)
95+
private void ScheduleNaturalSuccessors(in StackState currentState)
10896
{
10997
var instruction = _body.Instructions[currentState.InstructionIndex];
11098

@@ -134,6 +122,10 @@ private void ScheduleSuccessors(in StackState currentState)
134122
// Schedule branch target.
135123
switch (instruction.Operand)
136124
{
125+
case ICilLabel label:
126+
ScheduleLabel(currentState.InstructionIndex, label, nextStackSize);
127+
break;
128+
137129
case sbyte delta:
138130
ScheduleDelta(currentState.InstructionIndex, delta, nextStackSize);
139131
break;
@@ -142,10 +134,6 @@ private void ScheduleSuccessors(in StackState currentState)
142134
ScheduleDelta(currentState.InstructionIndex, delta, nextStackSize);
143135
break;
144136

145-
case ICilLabel label:
146-
ScheduleLabel(currentState.InstructionIndex, label, nextStackSize);
147-
break;
148-
149137
default:
150138
throw new NotSupportedException(
151139
$"Invalid or unsupported operand type at offset IL_{instruction.Offset:X4}.");
@@ -197,6 +185,38 @@ private void ScheduleSuccessors(in StackState currentState)
197185
}
198186
}
199187

188+
private void ScheduleExceptionalSuccessors(in StackState currentState)
189+
{
190+
var instruction = _body.Instructions[currentState.InstructionIndex];
191+
192+
// Did we just enter at least one exception handler try block?
193+
if (_handlers is null || !_handlers.TryGetValue(instruction.Offset, out var handlers))
194+
return;
195+
196+
// ECMA-335 Section I.12.4.2.8.1 prohibits entering try blocks with a non-zero stack size.
197+
if (currentState.StackSize != 0)
198+
throw new StackImbalanceException(_body, instruction.Offset);
199+
200+
// Schedule handler starts.
201+
foreach (var handler in handlers)
202+
{
203+
// Determine stack size at the start of the handler block.
204+
int stackDelta = handler.HandlerType switch
205+
{
206+
CilExceptionHandlerType.Exception => 1,
207+
CilExceptionHandlerType.Filter => 1,
208+
CilExceptionHandlerType.Finally => 0,
209+
CilExceptionHandlerType.Fault => 0,
210+
_ => throw new ArgumentOutOfRangeException(nameof(handler.HandlerType))
211+
};
212+
213+
if (handler.HandlerStart is { } handlerStart)
214+
ScheduleLabel(currentState.InstructionIndex, handlerStart, stackDelta);
215+
if (handler.FilterStart is { } filterStart)
216+
ScheduleLabel(currentState.InstructionIndex, filterStart, 1);
217+
}
218+
}
219+
200220
private void ScheduleLabel(int currentIndex, ICilLabel label, int nextStackSize)
201221
{
202222
int nextIndex = _body.Instructions.GetIndexByOffset(label.Offset);

test/AsmResolver.DotNet.Tests/Code/Cil/CilMethodBodyTest.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,91 @@ public void LeaveInstructionShouldClearStackAndNotFail()
321321
Assert.Equal(4, body.ComputeMaxStack());
322322
}
323323

324+
[Fact]
325+
public void UnreachableBlockShouldNotBeScheduled()
326+
{
327+
var body = CreateDummyBody(true);
328+
var il = body.Instructions;
329+
330+
// Exit early.
331+
il.Add(CilOpCodes.Ret);
332+
333+
// Create unreachable deliberate stack underflow.
334+
il.Add(CilOpCodes.Pop);
335+
il.Add(CilOpCodes.Ret);
336+
337+
// Max stack computation should succeed.
338+
Assert.Equal(0, body.ComputeMaxStack());
339+
}
340+
341+
[Fact]
342+
public void UnreachableExceptionHandlerShouldNotBeScheduled()
343+
{
344+
var start = new CilInstructionLabel();
345+
var handler = new CilInstructionLabel();
346+
var end = new CilInstructionLabel();
347+
348+
var body = CreateDummyBody(true);
349+
var il = body.Instructions;
350+
351+
// Exit early.
352+
il.Add(CilOpCodes.Ret);
353+
354+
// Create unreachable try-catch with deliberate stack underflow in try block.
355+
start.Instruction = il.Add(CilOpCodes.Pop);
356+
il.Add(CilOpCodes.Leave, end);
357+
handler.Instruction = il.Add(CilOpCodes.Leave, end);
358+
end.Instruction = il.Add(CilOpCodes.Ret);
359+
360+
body.ExceptionHandlers.Add(new CilExceptionHandler
361+
{
362+
HandlerType = CilExceptionHandlerType.Exception,
363+
ExceptionType = body.Owner!.Module!.CorLibTypeFactory.Object.ToTypeDefOrRef(),
364+
TryStart = start,
365+
TryEnd = handler,
366+
HandlerStart = handler,
367+
HandlerEnd = end
368+
});
369+
370+
// Max stack computation should succeed.
371+
Assert.Equal(0, body.ComputeMaxStack());
372+
}
373+
374+
[Fact]
375+
public void EnterTryBlockWithNonEmptyStackShouldThrow()
376+
{
377+
// https://github.com/Washi1337/AsmResolver/issues/652
378+
379+
var start = new CilInstructionLabel();
380+
var handler = new CilInstructionLabel();
381+
var end = new CilInstructionLabel();
382+
383+
var body = CreateDummyBody(true);
384+
var il = body.Instructions;
385+
386+
// Push value.
387+
il.Add(CilOpCodes.Ldc_I4, 1337);
388+
389+
// Create try block that consumes value
390+
start.Instruction = il.Add(CilOpCodes.Pop);
391+
il.Add(CilOpCodes.Leave, end);
392+
handler.Instruction = il.Add(CilOpCodes.Leave, end);
393+
end.Instruction = il.Add(CilOpCodes.Ret);
394+
395+
body.ExceptionHandlers.Add(new CilExceptionHandler
396+
{
397+
HandlerType = CilExceptionHandlerType.Exception,
398+
ExceptionType = body.Owner!.Module!.CorLibTypeFactory.Object.ToTypeDefOrRef(),
399+
TryStart = start,
400+
TryEnd = handler,
401+
HandlerStart = handler,
402+
HandlerEnd = end
403+
});
404+
405+
// Max stack computation should succeed.
406+
Assert.ThrowsAny<StackImbalanceException>(() => body.ComputeMaxStack());
407+
}
408+
324409
[Fact]
325410
public void LazyInitializationTest()
326411
{

0 commit comments

Comments
 (0)