Support Nesting of Exception Handlers (#199)

* partial progress

* progress

* progress

* progress

* fix

* add another example

* further progress

* formatting fix

* fixes

* undo removal of comment

* example null deref

* comment edit

* fix

* add unit test for this

* fix test

* fix

* bug fix

* bug fix

* fix bug

* undo debug stuff

* typo

* fix

* remove stash markers

* testclass introduction of bug fix

* doc fix

* undo regular control flow from throw through finally

* remove unecessary method

---------

Co-authored-by: Xiaoyu Liu <lixiaoyu@microsoft.com>
This commit is contained in:
Matthew Jin 2023-02-16 14:53:57 -08:00 коммит произвёл GitHub
Родитель 8f4efbe71c
Коммит df9aea035c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 637 добавлений и 287 удалений

Просмотреть файл

@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
namespace Cilsil.Test.Assets
@ -379,6 +380,51 @@ namespace Cilsil.Test.Assets
return returnValue;
}
/// <summary>
/// Tests control flow translation for a method with nested exception handlers.
/// </summary>
/// <param name="createNullDeref">If <c>true</c>, the method logic yields a null
/// dereference. Otherwise, it does not.</param>
public static void NestedExceptionConditionalNullDeref(bool createNullDeref)
{
object x = new object();
try
{
try
{
Console.Write("First try catch");
throw new Exception();
}
catch (Exception)
{
try
{
Console.Write("Before finally");
throw new Exception();
}
catch (Exception)
{
x = createNullDeref ? new object() : null;
}
finally
{
Console.Write("Inner try catch finally");
x = createNullDeref ? null : new object();
}
}
finally
{
Console.Write("Outer try catch finally");
throw new Exception();
}
Console.Write("Last instruction of outer try catch");
}
catch (Exception)
{
x.GetHashCode();
}
}
/// <summary>
/// No resource leak should be reported, as the allocated stream is closed in finally.
/// </summary>
@ -405,5 +451,26 @@ namespace Cilsil.Test.Assets
ThrowsIOException();
}
}
/// <summary>
/// We introduce this case for coverage of a false positive on stream; the return
/// statement in the try block was not routing through the finally block first.
/// </summary>
public static int LeaveEndTryCatchNeedToGoFinally()
{
var output = new TestClass().GetHashCode();
using (var stream = new FileStream("", FileMode.Create))
{
try
{
var x = 5;
return output;
}
catch (Exception ex)
{
throw ex;
}
}
}
}
}

Просмотреть файл

@ -78,6 +78,7 @@ namespace Cilsil.Test.Assets
InitializeInstanceObjectField,
InitializeStreamReaderObjectField,
InitializeInstanceObjectFieldViaReference,
NestedExceptionConditionalNullDeref,
ReturnElementFromInstanceArrayField,
ReturnOneDimArray,
ReturnTwoDimArray,
@ -488,6 +489,13 @@ namespace Cilsil.Test.Assets
throw new ArgumentException("TestCastClass requires 1 argument.");
}
return GetMethodCall(true);
case TestClassMethod.NestedExceptionConditionalNullDeref:
if (args == null || args.Length != 1)
{
throw new ArgumentException(
"NestedExceptionConditionalNullDeref requires 1 argument.");
}
return GetMethodCall(true);
default:
throw new NotImplementedException("Unhandled TestClassMethod.");
}

Просмотреть файл

@ -806,7 +806,7 @@ namespace Cilsil.Test.E2E
/// <summary>
/// Validates our translation of CastClass.
/// </summary>
/// <param name="testInputCode">Defines the object to be input to the type-checking
/// <param name="inputObjectString">Defines the object to be input to the type-checking
/// TestClass method; 0 for successful casting operation. 1 for casting exception.</param>
/// <param name="expectedError">The expected error.</param>
[DataRow("new TestClass()", InferError.None)]
@ -824,5 +824,26 @@ namespace Cilsil.Test.E2E
inputObjectString
})), GetString(expectedError));
}
/// <summary>
/// Validates translation of nested exception handler control flow.
/// </summary>
/// <param name="doNullDeref">If <c>true</c>, invoked method creates the null
/// deref. Else, does not.</param>
/// <param name="expectedError">The expected error.</param>
[DataRow(false, InferError.None)]
[DataRow(true, InferError.NULL_DEREFERENCE)]
[DataTestMethod]
public void NullExceptionTestNestedException(bool doNullDeref, InferError expectedError)
{
TestRunManager.Run(CallTestClassMethod(
TestClassMethod.NestedExceptionConditionalNullDeref,
true,
args: new string[]
{
doNullDeref.ToString()
.ToLower()
}), GetString(expectedError));
}
}
}

Просмотреть файл

@ -168,20 +168,20 @@ namespace Cilsil.Cil.Parsers
var callArgs = new List<Call.CallArg> { new Call.CallArg(arg.Item1, arg.Item2) };
return setLockedAttribute ? new Call(returnIdentifier,
new Tvoid(),
new ConstExpression(
ProcedureName.BuiltIn__set_locked_attribute),
callArgs,
callFlags,
state.CurrentLocation)
: new Call(returnIdentifier,
new Tvoid(),
new ConstExpression(
ProcedureName.
new Tvoid(),
new ConstExpression(
ProcedureName.BuiltIn__set_locked_attribute),
callArgs,
callFlags,
state.CurrentLocation)
: new Call(returnIdentifier,
new Tvoid(),
new ConstExpression(
ProcedureName.
BuiltIn__delete_locked_attribute),
callArgs,
callFlags,
state.CurrentLocation);
callArgs,
callFlags,
state.CurrentLocation);
}
}
}

Просмотреть файл

@ -15,22 +15,55 @@ namespace Cilsil.Cil.Parsers
// Throw can end a finally block but it is discouraged by the compiler and should
// be rare.
case Code.Endfinally:
var exceptionHandler = state.MethodExceptionHandlers
.GetExceptionHandlerAtInstruction(instruction);
// This instruction was reached through non-exceptional control flow.
if (!state.FinallyExceptionalTranslation)
{
// We continue translation with that operand from the end of the finally
// block, now that finally block has been translated.
state.PushInstruction(state.EndfinallyControlFlow);
// In this case, we need to route control through the next finally block.
if (exceptionHandler != null &&
exceptionHandler.HandlerType == ExceptionHandlerType.Finally)
{
state.PushInstruction(exceptionHandler.HandlerStart);
}
else
{
// In this case, translation of the finally block was prompted by a
// throw instruction; we then terminate the CFG branch with the throw
// node via RegisterNode.
if (state.EndfinallyThrowNode != null)
{
RegisterNode(state, state.EndfinallyThrowNode);
state.EndfinallyThrowNode = null;
}
else
{
// We continue translation with that operand from the end of the
// finally block, now that finally block has been translated.
state.PushInstruction(state.EndfinallyControlFlow);
}
}
}
// This instruction was reached through exceptional control flow.
else
{
var handler =
state.MethodExceptionHandlers.FinallyEndToHandler[instruction];
var handler = state.MethodExceptionHandlers.FinallyEndToHandler[instruction];
if (!state.FinallyHandlerToExceptionExit.ContainsKey(handler))
{
state.FinallyHandlerToExceptionExit[handler] =
CreateFinallyExceptionExitNode(state, handler);
var exceptionExitNode = CreateFinallyExceptionExitNode(state, handler);
state.FinallyHandlerToExceptionExit[handler] = exceptionExitNode;
// We route control flow through the next finally handler, if there is
// one.
if (exceptionHandler != null &&
exceptionHandler.HandlerType == ExceptionHandlerType.Finally)
{
var finallyBranchNode =
CreateFinallyExceptionBranchNode(state, handler);
exceptionExitNode.Successors.Add(finallyBranchNode);
state.PushInstruction(exceptionHandler.HandlerStart,
finallyBranchNode);
}
}
else
{

Просмотреть файл

@ -10,6 +10,7 @@ using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using ProcStack = System.Collections.Generic.Stack<(Cilsil.Sil.Expressions.Expression Expression,
@ -124,12 +125,19 @@ namespace Cilsil.Cil.Parsers
state.Cfg.RegisterNode(node);
state.PreviousNode.Successors.Add(node);
node.BlockEndOffset = state.MethodExceptionHandlers
.GetBlockEndOffsetFromOffset(
state.CurrentInstruction.Offset);
if (state.MethodExceptionHandlers.GetExceptionHandlerAtInstruction(
state.CurrentInstruction) != null)
.GetBlockEndOffsetFromInstruction(
state.CurrentInstruction);
var exceptionHandler = state.MethodExceptionHandlers
.GetExceptionHandlerAtInstruction(state.CurrentInstruction);
if (exceptionHandler != null)
{
state.NodesToLinkWithExceptionBlock.Add(node);
var exceptionHandlerEntryNode = GetHandlerExceptionalEntryBlock(state, exceptionHandler);
if (exceptionHandlerEntryNode != null)
{
node.ExceptionNodes.Add(exceptionHandlerEntryNode);
}
}
if (RememberNodeOffset)
{
@ -273,6 +281,11 @@ namespace Cilsil.Cil.Parsers
node.ExceptionNodes.Add(entryNode);
// Note: this isn't a perfect way of trying to separate exceptional control
// flow from regular control flow; because translation proceeds via DFS, this
// distinction may not work in the event of i.e. nested finally blocks.
// However, this seems not to pose a material issue for correct detection of
// issues.
state.FinallyExceptionalTranslation = true;
state.PushInstruction(handler.HandlerStart, node);
state.FinallyExceptionalTranslation = false;
@ -325,14 +338,21 @@ namespace Cilsil.Cil.Parsers
// exception type-matching node.
else
{
if (handlerNode.FinallyBlock != null)
var exceptionHandler =
state.MethodExceptionHandlers
.GetExceptionHandlerAtInstruction(handlerNode.ExceptionHandler
.HandlerStart);
// If there is a finally exception handler associated with this catch handler, we
// route regular control flow through the finally block.
if (exceptionHandler != null &&
exceptionHandler.HandlerType == ExceptionHandlerType.Finally)
{
var finallyBranchNode = CreateFinallyExceptionBranchNode(
state, handlerNode.ExceptionHandler);
falseBranch.Successors
.Add(finallyBranchNode);
(var finallyLoadCatchVar, _) = GetHandlerCatchVarNode(
state, handlerNode.FinallyBlock);
state, exceptionHandler);
finallyBranchNode.Successors.Add(finallyLoadCatchVar);
}
else
@ -372,7 +392,7 @@ namespace Cilsil.Cil.Parsers
return state.ExceptionHandlerToCatchVarNode[handler];
}
protected static CfgNode CreateFinallyExceptionalEntryBlock(ProgramState state,
private static CfgNode CreateFinallyExceptionalEntryBlock(ProgramState state,
ExceptionHandler handler)
{
(var entryNode, _) = GetHandlerEntryNode(state, handler);
@ -521,11 +541,11 @@ namespace Cilsil.Cil.Parsers
isInstCall, pruneFalseInstruction
});
pruneTrueNode.BlockEndOffset = state.MethodExceptionHandlers
.GetBlockEndOffsetFromOffset(
state.CurrentInstruction.Offset);
.GetBlockEndOffsetFromInstruction(
state.CurrentInstruction);
pruneFalseNode.BlockEndOffset = state.MethodExceptionHandlers
.GetBlockEndOffsetFromOffset(
state.CurrentInstruction.Offset);
.GetBlockEndOffsetFromInstruction(
state.CurrentInstruction);
state.Cfg.RegisterNode(pruneTrueNode);
state.Cfg.RegisterNode(pruneFalseNode);
return (pruneTrueNode, pruneFalseNode);
@ -553,18 +573,72 @@ namespace Cilsil.Cil.Parsers
new ExnExpression(returnValue),
Typ.FromTypeReference(retType),
location);
retNode.Instructions.Add(retInstr);
retNode.Successors = new List<CfgNode> { state.ProcDesc.ExitNode };
if (state.CurrentInstruction.OpCode.Code == Code.Throw)
{
var builtinFunctionExpression = new ConstExpression(ProcedureName.BuiltIn__throw);
var throwCall = new Call(state.GetIdentifier(Identifier.IdentKind.Normal),
new Tvoid(),
builtinFunctionExpression,
new List<Call.CallArg>(),
new Call.CallFlags(),
state.CurrentLocation);
retNode.Instructions.Add(throwCall);
}
return retNode;
}
protected static void CreateExceptionalEdges(ProgramState state, CfgNode entryNode)
/// <summary>
/// Gets the entry node for the exception handler, creating the entry block if needed.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="handler">The catch handler for which we are potentially creating
/// exceptional control flow.</param>
protected static CfgNode GetHandlerExceptionalEntryBlock(ProgramState state,
ExceptionHandler handler)
{
foreach (var node in state.NodesToLinkWithExceptionBlock)
if (!(handler.HandlerType == ExceptionHandlerType.Catch ||
handler.HandlerType == ExceptionHandlerType.Finally))
{
node.ExceptionNodes.Add(entryNode);
return null;
}
state.NodesToLinkWithExceptionBlock = new List<CfgNode>();
CfgNode entryNode;
Identifier exceptionIdentifier;
var exnInfo = state.MethodExceptionHandlers;
var instruction = handler.TryEnd.Previous;
if (!state.ExceptionHandlerSetToEntryNode.ContainsKey(handler))
{
if (handler.HandlerType == ExceptionHandlerType.Catch)
{
(entryNode, exceptionIdentifier) = GetHandlerEntryNode(
state, exnInfo.TryOffsetToCatchHandlers[instruction.Offset]
.Item1[0]
.ExceptionHandler);
// Exceptional control flow routes through the first of the set of
// associated catch handlers; this invocation pushes the catch
// handler's first instruction onto the stack and continues the
// translation from the handler's catch variable load node.
CreateCatchHandlerEntryBlock(
state,
exnInfo.TryOffsetToCatchHandlers[instruction.Offset]
.Item1[0],
entryNode,
exceptionIdentifier);
}
// Is a finally handler.
else
{
entryNode = CreateFinallyExceptionalEntryBlock(state, handler);
}
}
else
{
(entryNode, _) = state.ExceptionHandlerSetToEntryNode[handler];
}
return entryNode;
}
/// <summary>

Просмотреть файл

@ -19,99 +19,47 @@ namespace Cilsil.Cil.Parsers
// Leave for catch block: target instruction immediately after catch block.
case Code.Leave:
case Code.Leave_S:
case Code.Throw:
var exnInfo = state.MethodExceptionHandlers;
// This is null if the instruction is throw.
var mapType = exnInfo.GetMapTypeFromInstruction(instruction);
// This is null only if the instruction is throw, which is handled in a
// different case.
var target = instruction.Operand as Instruction;
if (instruction.OpCode.Code == Code.Throw)
{
(var returnValue, _) = state.Pop();
var retNode = CreateExceptionReturnNode(state,
returnValue,
state.CurrentLocation);
RegisterNode(state, retNode);
}
// Leave within try of catch-block.
if (exnInfo.TryOffsetToCatchHandlers.ContainsKey(instruction.Offset))
if (mapType == MethodExceptionHandlers.MapType.TryToCatch)
{
if (state.NodesToLinkWithExceptionBlock.Count > 0)
HandleFinallyControlFlowForTryToCatchLeave(state, instruction, target);
}
// Leave occurs within catch block.
else if (mapType == MethodExceptionHandlers.MapType.CatchToCatch)
{
var exceptionHandler =
exnInfo.GetExceptionHandlerAtInstruction(instruction);
// We now handle regular control flow if there's a finally block. Note,
// theoretically we might use the same helper method as we do for
// TryToCatch, but not completely sure that it would be equivalent.
if (exceptionHandler != null &&
exceptionHandler.HandlerType == ExceptionHandlerType.Finally)
{
CfgNode entryNode;
Identifier exceptionIdentifier;
if (!state.LeaveToExceptionEntryNode.ContainsKey(instruction))
{
(entryNode, exceptionIdentifier) = GetHandlerEntryNode(
state, exnInfo.TryOffsetToCatchHandlers[instruction.Offset][0]
.ExceptionHandler);
state.LeaveToExceptionEntryNode[instruction] =
(entryNode, exceptionIdentifier);
// Exceptional control flow routes through the first of the set of
// associated catch handlers; this invocation pushes the catch
// handler's first instruction onto the stack and continues the
// translation from the handler's catch variable load node.
CreateCatchHandlerEntryBlock(
state,
exnInfo.TryOffsetToCatchHandlers[instruction.Offset][0],
entryNode,
exceptionIdentifier);
}
else
{
(entryNode, _) =
state.LeaveToExceptionEntryNode[instruction];
}
CreateExceptionalEdges(state, entryNode);
state.PushInstruction(
exceptionHandler.HandlerStart,
CreateFinallyHandlerNonExceptionalEntry(
state, exceptionHandler, target));
}
if (target != null)
else
{
// Control flow routes directly to the target, as there is no finally
// block through which to first route it.
state.PushInstruction(target);
}
}
// Leave occurs within catch block.
else if (exnInfo.CatchOffsetToCatchHandler.ContainsKey(instruction.Offset))
// Narrowest containing interval for offset is the try of a finally block,
// since leave can't occur within a finally handler.
else if (mapType == MethodExceptionHandlers.MapType.TryToFinally)
{
var currentHandler = exnInfo.CatchOffsetToCatchHandler[instruction.Offset];
// Exceptional control flow routes through the finally block, if present,
// prior to routing control flow to leave.
if (currentHandler.FinallyBlock != null &&
state.NodesToLinkWithExceptionBlock.Count > 0)
{
var finallyEntryNode = CreateFinallyExceptionalEntryBlock(
state, currentHandler.FinallyBlock);
CreateExceptionalEdges(state, finallyEntryNode);
}
if (target != null)
{
if (currentHandler.FinallyBlock != null)
{
state.PushInstruction(
currentHandler.FinallyBlock.HandlerStart,
CreateFinallyHandlerNonExceptionalEntry(
state, currentHandler.FinallyBlock, target));
}
else
{
// Control flow routes directly to the target, as there is no finally
// block through which to first route it.
state.PushInstruction(target);
}
}
}
// Leave occurs within try of finally block (we leave this as the last option,
// as the try block of a finally encompasses all of the try-catch bytecode, in
// the case of try-catch-finally.
else if (exnInfo.TryOffsetToFinallyHandler.ContainsKey(instruction.Offset))
{
var finallyHandler = exnInfo.TryOffsetToFinallyHandler[instruction.Offset];
if (state.NodesToLinkWithExceptionBlock.Count > 0)
{
var finallyEntryNode =
CreateFinallyExceptionalEntryBlock(state, finallyHandler);
CreateExceptionalEdges(state, finallyEntryNode);
}
var finallyHandler =
exnInfo.TryOffsetToFinallyHandler[instruction.Offset].Item1;
if (target != null)
{
state.PushInstruction(finallyHandler.HandlerStart,
@ -120,6 +68,29 @@ namespace Cilsil.Cil.Parsers
}
}
return true;
case Code.Throw:
(var returnValue, _) = state.Pop();
var retNode = CreateExceptionReturnNode(state,
returnValue,
state.CurrentLocation);
RegisterNode(state, retNode);
return true;
case Code.Rethrow:
var exceptionType =
state.MethodExceptionHandlers
.CatchOffsetToCatchHandler[instruction.Offset]
.Item1
.ExceptionHandler
.CatchType;
(var memoryAllocationCall, var objectVariable) = CreateMemoryAllocationCall(
exceptionType, state);
state.PreviousNode.Instructions.Add(memoryAllocationCall);
var rethrowNode = CreateExceptionReturnNode(state,
objectVariable,
state.CurrentLocation);
RegisterNode(state, rethrowNode);
return true;
default:
return false;
}
@ -128,9 +99,9 @@ namespace Cilsil.Cil.Parsers
/// <summary>
/// Creates a new node to ensure finally instructions aren't attached to a body node.
/// </summary>
private static CfgNode CreateFinallyHandlerNonExceptionalEntry(ProgramState state,
ExceptionHandler handler,
Instruction leaveTarget)
private static CfgNode CreateFinallyHandlerNonExceptionalEntry(
ProgramState state, ExceptionHandler handler,
Instruction leaveTarget, CfgNode endFinallyThrowNode = null)
{
CfgNode finallyHandlerStartNode = null;
(var nodeOffset, _) = state.GetOffsetNode(handler.HandlerStart.Offset);
@ -144,9 +115,42 @@ namespace Cilsil.Cil.Parsers
state.PreviousNode.Successors.Add(finallyHandlerStartNode);
state.AppendToPreviousNode = true;
}
state.EndfinallyControlFlow = leaveTarget;
if (leaveTarget != null)
{
state.EndfinallyControlFlow = leaveTarget;
}
if (endFinallyThrowNode != null)
{
state.EndfinallyThrowNode = endFinallyThrowNode;
}
return finallyHandlerStartNode;
}
private void HandleFinallyControlFlowForTryToCatchLeave(
ProgramState state, Instruction currentInstr, Instruction targetInstr)
{
var exnInfo = state.MethodExceptionHandlers;
exnInfo.TryOffsetToFinallyHandler.TryGetValue(
currentInstr.Offset, out var currentFinallyHandler);
exnInfo.TryOffsetToFinallyHandler.TryGetValue(
targetInstr.Offset, out var targetFinallyHandler);
// If target is associated with a different finally handler than is
// the present instruction, then we need to route control flow through
// this finally block first as we're leaving the finally handler. If we just use
// GetExceptionHandlerAtInstruction, this would just yield a catch block, obscuring the
// need to first direct flow through the finally block.
if (currentFinallyHandler.Item1?.HandlerStart?.Offset !=
targetFinallyHandler.Item1?.HandlerStart?.Offset)
{
state.PushInstruction(
currentFinallyHandler.Item1.HandlerStart,
CreateFinallyHandlerNonExceptionalEntry(
state, currentFinallyHandler.Item1, targetInstr));
}
else
{
state.PushInstruction(targetInstr);
}
}
}
}

Просмотреть файл

@ -159,9 +159,7 @@ namespace Cilsil.Services
var methodBody = method.Body;
var unhandledExceptionCase =
programState.MethodExceptionHandlers.UnhandledExceptionBlock ||
!programState.MethodExceptionHandlers.NoNestedTryCatchFinally() ||
!programState.MethodExceptionHandlers.NoFinallyEndWithThrow();
programState.MethodExceptionHandlers.UnhandledExceptionBlock;
// True if the translation terminates early, false otherwise.
var translationUnfinished = false;
@ -196,19 +194,16 @@ namespace Cilsil.Services
MethodExceptionHandlers.DefaultHandlerEndOffset);
// We don't reuse nodes of finally handlers.
if (nodeAtOffset != null &&
!programState.MethodExceptionHandlers
.FinallyOffsetToFinallyHandler
.ContainsKey(nextInstruction.Offset) &&
!programState.MethodExceptionHandlers
.CatchOffsetToCatchHandler
.ContainsKey(nextInstruction.Offset))
programState.MethodExceptionHandlers
.GetMapTypeFromInstruction(nextInstruction)
!= MethodExceptionHandlers.MapType.CatchToCatch)
{
programState.PreviousNode.Successors.Add(nodeAtOffset);
}
else if (unhandledExceptionCase)
{
Log.WriteWarning($"Unhandled exception-handling.");
Log.RecordUnknownInstruction("unhandled-exception");
Log.RecordUnknownInstruction("unhandled-exception");
Log.RecordUnfinishedMethod(programState.Method.GetCompatibleFullName(),
nextInstruction.RemainingInstructionCount());
translationUnfinished = true;

Просмотреть файл

@ -97,6 +97,16 @@ namespace Cilsil.Sil
BuiltInClassName,
string.Empty,
true);
/// <summary>
/// Standard procedure name for throw calls, interpreted specially by the backend.
/// </summary>
public static readonly ProcedureName BuiltIn__throw =
new ProcedureName("__throw",
new List<string>(),
BuiltInClassName,
string.Empty,
true);
#endregion
/// <summary>

Просмотреть файл

@ -21,10 +21,6 @@ namespace Cilsil.Utils
/// </summary>
public ExceptionHandlerNode PreviousCatchBlock;
/// <summary>
/// The finally block.
/// </summary>
public ExceptionHandler FinallyBlock;
/// <summary>
/// The first catch handler.
/// </summary>
public ExceptionHandler FirstCatchHandler = null;
@ -34,14 +30,11 @@ namespace Cilsil.Utils
/// </summary>
/// <param name="exceptionHandler">The exception handler.</param>
/// <param name="nextCatchBlock">The next catch clause.</param>
/// <param name="finallyBlock">The finally block.</param>
public ExceptionHandlerNode(ExceptionHandler exceptionHandler,
ExceptionHandlerNode nextCatchBlock = null,
ExceptionHandler finallyBlock = null)
ExceptionHandlerNode nextCatchBlock = null)
{
ExceptionHandler = exceptionHandler;
NextCatchBlock = nextCatchBlock;
FinallyBlock = finallyBlock;
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using Mono.Cecil.Cil;
using System.Collections.Generic;
using static Cilsil.Utils.MethodExceptionHandlers;
namespace Cilsil.Utils
{
@ -42,34 +43,94 @@ namespace Cilsil.Utils
/// <summary>
/// Maps offsets within try-blocks of try-catch to the corresponding catch handler set.
/// </summary>
public readonly Dictionary<int, List<ExceptionHandlerNode>> TryOffsetToCatchHandlers;
public readonly Dictionary<int,
(List<ExceptionHandlerNode>, int)> TryOffsetToCatchHandlers;
/// <summary>
/// Maps offsets within try-blocks of try-finally to the corresponding finally handler.
/// </summary>
public readonly Dictionary<int, ExceptionHandler> TryOffsetToFinallyHandler;
public readonly Dictionary<int, (ExceptionHandler, int)> TryOffsetToFinallyHandler;
/// <summary>
/// Maps offsets within catch-blocks to the corresponding catch handler.
/// </summary>
public readonly Dictionary<int, ExceptionHandlerNode> CatchOffsetToCatchHandler;
public readonly Dictionary<int, (ExceptionHandlerNode, int)> CatchOffsetToCatchHandler;
/// <summary>
/// Maps offsets within finally-blocks to the corresponding finally handler.
/// Maps offsets within finally-blocks to the corresponding finally handler and the .
/// </summary>
public readonly Dictionary<int, ExceptionHandler> FinallyOffsetToFinallyHandler;
public readonly Dictionary<int, (ExceptionHandler, int)> FinallyOffsetToFinallyHandler;
#endregion
/// <summary>
/// Used for describing the most closely nested handler for an offset.
/// </summary>
public enum MapType
{
/// <summary>
/// Try offset to corresponding catch handler.
/// </summary>
TryToCatch,
/// <summary>
/// Try offset to corresponding finally handler.
/// </summary>
TryToFinally,
/// <summary>
/// Catch offset to corresponding catch handler.
/// </summary>
CatchToCatch,
/// <summary>
/// Finally offset to corresponding finally handler.
/// </summary>
FinallyToFinally,
/// <summary>
/// No handler associated with offset.
/// </summary>
None
}
/// <summary>
/// Computes the narrowest interval containing the given offset.
/// </summary>
/// <param name="instruction">The offset for which to contain the interval.</param>
/// <returns>The MapType corresponding to the narrowest interval at the given
/// offset.</returns>
public MapType GetMapTypeFromInstruction(Instruction instruction)
{
var offset = instruction.Offset;
var mapType = MapType.None;
var intervalContainingOffsetSize = int.MaxValue;
if (TryOffsetToCatchHandlers.ContainsKey(offset) &&
TryOffsetToCatchHandlers[offset].Item2 < intervalContainingOffsetSize)
{
mapType = MapType.TryToCatch;
intervalContainingOffsetSize = TryOffsetToCatchHandlers[offset].Item2;
}
if (TryOffsetToFinallyHandler.ContainsKey(offset) &&
TryOffsetToFinallyHandler[offset].Item2 < intervalContainingOffsetSize)
{
mapType = MapType.TryToFinally;
intervalContainingOffsetSize = TryOffsetToFinallyHandler[offset].Item2;
}
if (CatchOffsetToCatchHandler.ContainsKey(offset) &&
CatchOffsetToCatchHandler[offset].Item2 < intervalContainingOffsetSize)
{
mapType = MapType.CatchToCatch;
intervalContainingOffsetSize = CatchOffsetToCatchHandler[offset].Item2;
}
if (FinallyOffsetToFinallyHandler.ContainsKey(offset) &&
FinallyOffsetToFinallyHandler[offset].Item2 < intervalContainingOffsetSize)
{
mapType = MapType.FinallyToFinally;
}
return mapType;
}
/// <summary>
/// The default handler end offset, overwritten when it can be found.
/// </summary>
public const int DefaultHandlerEndOffset = -1;
private static bool InstructionBlockWithinBounds(
(Instruction start, Instruction end) block,
(Instruction start, Instruction end) bounds) =>
block.start.Offset >= bounds.start.Offset && block.end.Offset <= bounds.end.Offset;
/// <summary>
/// <c>true</c> if there is an unsupported exception block; <c>false</c> otherwise.
/// </summary>
@ -149,22 +210,10 @@ namespace Cilsil.Utils
}
}
foreach (var catchTry in TryBoundsToCatchHandlers.Keys)
{
foreach (var finallyTry in TryBoundsToFinallyHandlers.Keys)
{
if (InstructionBlockWithinBounds(catchTry, finallyTry))
{
foreach (var handler in TryBoundsToCatchHandlers[catchTry])
{
handler.FinallyBlock = TryBoundsToFinallyHandlers[finallyTry];
}
}
// Each set of handlers can map to only one finally block.
continue;
}
}
// A different offset does not necessarily indicate a different instruction -- there
// are key values (offsets) in these dictionaries which don't actually refer to
// instructions. This doesn't create any issue with the representation of actual
// existing instructions.
TryOffsetToCatchHandlers = ConvertBoundsToOffsets(TryBoundsToCatchHandlers);
TryOffsetToFinallyHandler = ConvertBoundsToOffsets(TryBoundsToFinallyHandlers);
CatchOffsetToCatchHandler = ConvertBoundsToOffsets(CatchBoundsToCatchHandler);
@ -172,100 +221,44 @@ namespace Cilsil.Utils
}
/// <summary>
/// Returns the exception handler associated with the instruction -- a catch handler in the
/// case of an instruction within the try block of a try-catch or try-catch-finally or a
/// finally handler for try-finally, and a finally handler in the case of a catch
/// instruction in try-catch-finally. We assume there is no try-catch nested within a
/// finally block or in a catch-block.
/// Returns the most nested exception handler associated with the instruction.
/// </summary>
/// <param name="instruction">The instruction from which to determine the
/// exception-handling control flow.</param>
/// <returns>The exception handler in any of the cases described above.</returns>
public ExceptionHandler GetExceptionHandlerAtInstruction(Instruction instruction)
{
if (TryOffsetToCatchHandlers.ContainsKey(instruction.Offset))
var offset = instruction.Offset;
switch (GetMapTypeFromInstruction(instruction))
{
return TryOffsetToCatchHandlers[instruction.Offset][0].ExceptionHandler;
case MapType.TryToCatch:
return TryOffsetToCatchHandlers[offset].Item1[0].ExceptionHandler;
case MapType.TryToFinally:
return TryOffsetToFinallyHandler[offset].Item1;
case MapType.CatchToCatch:
case MapType.FinallyToFinally:
// Both handlers; we select the narrower handler.
if (TryOffsetToCatchHandlers.ContainsKey(offset) &&
TryOffsetToFinallyHandler.ContainsKey(offset))
{
return TryOffsetToCatchHandlers[offset].Item2 < TryOffsetToFinallyHandler[offset].Item2 ?
TryOffsetToCatchHandlers[offset].Item1[0].ExceptionHandler :
TryOffsetToFinallyHandler[offset].Item1;
}
// Catch handler but no finally handler.
if (TryOffsetToCatchHandlers.ContainsKey(offset))
{
return TryOffsetToCatchHandlers[offset].Item1[0].ExceptionHandler;
}
// Finally handler but no catch handler.
if (TryOffsetToFinallyHandler.ContainsKey(offset))
{
return TryOffsetToFinallyHandler[offset].Item1;
}
return null;
default:
return null;
}
else if (TryOffsetToFinallyHandler.ContainsKey(instruction.Offset))
{
return TryOffsetToFinallyHandler[instruction.Offset];
}
else if (CatchOffsetToCatchHandler.ContainsKey(instruction.Offset))
{
return CatchOffsetToCatchHandler[instruction.Offset].ExceptionHandler;
}
return null;
}
/// <summary>
/// This method determines that there are no nested exception-handling blocks, aside from
/// the catch block necessarily being nested in the try block of a try-catch-finally.
/// </summary>
/// <returns><c>true</c> if the catch try, catch, and finally block offsets are all
/// mutually exclusive.</returns>
public bool NoNestedTryCatchFinally()
{
// For any try-catch-finally block, the try associated with the finally block includes
// the whole catch block (and therefore also the try associated with the catch block).
var boundsList = new List<(int start, int end)>();
var finallyBoundsList = new List<(int start, int end)>();
foreach ((var tryStart, var tryEnd) in TryBoundsToCatchHandlers.Keys)
{
boundsList.Add((tryStart.Offset, tryEnd.Offset));
}
foreach (var handler in CatchBoundsToCatchHandler.Values)
{
boundsList.Add((handler.ExceptionHandler.HandlerStart.Offset,
handler.ExceptionHandler.HandlerEnd.Previous.Offset));
}
foreach (var handler in TryBoundsToFinallyHandlers.Values)
{
boundsList.Add((handler.HandlerStart.Offset,
handler.HandlerEnd.Previous.Offset));
finallyBoundsList.Add((handler.HandlerStart.Offset,
handler.HandlerEnd.Previous.Offset));
finallyBoundsList.Add((handler.TryStart.Offset,
handler.TryEnd.Previous.Offset));
}
boundsList.Sort((x, y) => x.end.CompareTo(y.end));
finallyBoundsList.Sort((x, y) => x.end.CompareTo(y.end));
for (int i = 1; i < boundsList.Count; i++)
{
if (boundsList[i - 1].end > boundsList[i].start)
{
return false;
}
}
for (int i = 1; i < finallyBoundsList.Count; i++)
{
if (finallyBoundsList[i - 1].end > finallyBoundsList[i].start)
{
return false;
}
}
return true;
}
/// <summary>
/// Used for excluding translation of methods with finally blocks ending in throw.
/// </summary>
/// <returns><c>true</c> if there is no finally block that ends with throw, <c>false</c>
/// otherwise.</returns>
public bool NoFinallyEndWithThrow()
{
foreach (var finallyEnd in FinallyEndToHandler.Keys)
{
if (finallyEnd.OpCode.Code == Code.Throw)
{
return false;
}
}
return true;
}
/// <summary>
@ -275,47 +268,56 @@ namespace Cilsil.Utils
/// of a try-catch, the try-catch try end is returned, as opposed to the try-finally
/// try end if there is an enclosing finally block.
/// </summary>
/// <param name="offset">The offset.</param>
/// <param name="instruction">The offset.</param>
/// <returns>The end offset of the most immediate surrounding exception-handling block;
/// returns the default value if the offset is not in a block.</returns>
public int GetBlockEndOffsetFromOffset(int offset)
public int GetBlockEndOffsetFromInstruction(Instruction instruction)
{
if (TryOffsetToCatchHandlers.ContainsKey(offset))
var offset = instruction.Offset;
var mapType = GetMapTypeFromInstruction(instruction);
if (mapType == MapType.TryToCatch)
{
return TryOffsetToCatchHandlers[offset][0].ExceptionHandler
.TryEnd
.Previous
.Offset;
return TryOffsetToCatchHandlers[offset].Item1[0]
.ExceptionHandler
.TryEnd
.Previous
.Offset;
}
else if (CatchOffsetToCatchHandler.ContainsKey(offset))
else if (mapType == MapType.CatchToCatch)
{
return CatchOffsetToCatchHandler[offset].ExceptionHandler
return CatchOffsetToCatchHandler[offset].Item1
.ExceptionHandler
.HandlerEnd
.Previous
.Offset;
}
else if (TryOffsetToFinallyHandler.ContainsKey(offset))
else if (mapType == MapType.TryToFinally)
{
return TryOffsetToFinallyHandler[offset].TryEnd.Previous.Offset;
return TryOffsetToFinallyHandler[offset].Item1.TryEnd.Previous.Offset;
}
else if (FinallyOffsetToFinallyHandler.ContainsKey(offset))
else if (mapType == MapType.FinallyToFinally)
{
return FinallyOffsetToFinallyHandler[offset].HandlerEnd.Previous.Offset;
return FinallyOffsetToFinallyHandler[offset].Item1.HandlerEnd.Previous.Offset;
}
return DefaultHandlerEndOffset;
;
}
private static Dictionary<int, T> ConvertBoundsToOffsets<T>(
private static Dictionary<int, (T, int)> ConvertBoundsToOffsets<T>(
Dictionary<(Instruction, Instruction), T> boundsToObject)
{
var converted = new Dictionary<int, T>();
var converted = new Dictionary<int, (T, int)>();
foreach ((var start, var end) in boundsToObject.Keys)
{
// We track the narrowest of all of the intervals of this type containing the
// offset.
for (int i = start.Offset; i <= end.Offset; i++)
{
converted[i] = boundsToObject[(start, end)];
if (!converted.ContainsKey(i) ||
end.Offset - start.Offset < converted[i].Item2)
{
converted[i] = (boundsToObject[(start, end)], end.Offset - start.Offset);
}
}
}
return converted;

Просмотреть файл

@ -129,13 +129,6 @@ namespace Cilsil.Utils
/// </summary>
private int NextAvailableSyntheticVariableId;
/// <summary>
/// List of nodes to link to an exception block when a leave instruction is encountered. If
/// the translation state is not in a try or catch block, translated body nodes don't need
/// to be recorded.
/// </summary>
public List<CfgNode> NodesToLinkWithExceptionBlock;
/// <summary>
/// <c>true</c> if the top instruction is in a try or catch block; <c>false</c> otherwise.
/// </summary>
@ -150,7 +143,7 @@ namespace Cilsil.Utils
(CfgNode node, LvarExpression variable)> ExceptionHandlerToCatchVarNode;
/// <summary>
/// The exception handler to its entry node as well as pthe identifier for the unwrapped
/// The exception handler to its entry node as well as the identifier for the unwrapped
/// exception.
/// </summary>
public Dictionary<ExceptionHandler,
@ -170,10 +163,10 @@ namespace Cilsil.Utils
public Instruction EndfinallyControlFlow;
/// <summary>
/// Maps a leave instruction offset to the exceptional entry node created for it, as well
/// as the associated identifier for the unwrapped exception.
/// Null by default. When control flow is exited via a throw instruction, this is the throw
/// node to be added at the end of the translation of the finally block, when one exists.
/// </summary>
public Dictionary<Instruction, (CfgNode, Identifier)> LeaveToExceptionEntryNode;
public CfgNode EndfinallyThrowNode;
/// <summary>
/// Contains information about the program's exception handlers.
@ -207,7 +200,6 @@ namespace Cilsil.Utils
ParsedInstructions = new List<Instruction>();
MethodExceptionHandlers = new MethodExceptionHandlers(method.Body);
NodesToLinkWithExceptionBlock = new List<CfgNode>();
InstructionInTryOrCatch = false;
OffsetToNode = new Dictionary<int, List<(CfgNode Node, ProgramStack Stack, int)>>();
@ -219,7 +211,6 @@ namespace Cilsil.Utils
FinallyHandlerToExceptionExit = new Dictionary<ExceptionHandler, CfgNode>();
ExceptionHandlerSetToEntryNode = new Dictionary<ExceptionHandler,
(CfgNode node, Identifier id)>();
LeaveToExceptionEntryNode = new Dictionary<Instruction, (CfgNode, Identifier)>();
FinallyExceptionalTranslation = false;
@ -242,8 +233,8 @@ namespace Cilsil.Utils
// and the previous node is from a different handler block, we empty the saved stack,
// as there should not be anything on the stack when transferring control between
// different handler blocks.
if (!MethodExceptionHandlers.CatchOffsetToCatchHandler
.ContainsKey(CurrentInstruction.Offset) &&
if (MethodExceptionHandlers.GetMapTypeFromInstruction(CurrentInstruction) !=
MethodExceptionHandlers.MapType.CatchToCatch &&
node.BlockEndOffset != PreviousNode.BlockEndOffset)
{
OffsetToNode

Просмотреть файл

@ -36,7 +36,7 @@ public class IsDisposedBooleanField : IDisposable
}
}
// Expect 2 TAINT_ERROR for SQL injection flows.
// Expect 4 TAINT_ERROR for SQL injection flows.
public class PulseTaintTests
{
[HttpPost]
@ -62,6 +62,32 @@ public class PulseTaintTests
{
subproj.WeatherForecast.runSqlCommandStoredProcedure(InputParameter.ToString());
}
[HttpGet]
public void SearchRawData(string query)
{
var queryPrefix = "prefix";
using (var conn = new SqlConnection("readerConnectionString"))
{
using (var command = new SqlCommand(queryPrefix + query))
{
try
{
var reader = command.ExecuteReader();
while (reader.Read())
{
Console.Write("Hello");
}
}
catch (Exception ex)
{
Console.Write(ex.Message);
return;
}
}
}
}
}
@ -110,6 +136,85 @@ public class ThreadSafety
{
return null;
}
public static void TestNullDerefExpectError()
{
object x = new object();
try
{
try
{
Console.Write("First try catch");
throw new Exception();
}
catch (Exception)
{
try
{
Console.Write("Before finally");
throw new Exception();
}
catch (Exception)
{
x = new object();
}
finally
{
Console.Write("Inner try catch finally");
x = null;
}
}
finally
{
Console.Write("Outer try catch finally");
throw new Exception();
}
Console.Write("Last instruction of outer try catch");
}
catch (Exception)
{
x.GetHashCode();
}
}
public static void TestNullDerefExpectNoError()
{
object x = new object();
try
{
try
{
Console.Write("First try catch");
throw new Exception();
}
catch (Exception)
{
try
{
Console.Write("Before finally");
throw new Exception();
}
catch (Exception)
{
x = null;
}
finally
{
Console.Write("Inner try catch finally");
x = new object();
}
}
finally
{
Console.Write("Outer try catch finally");
throw new Exception();
}
Console.Write("Last instruction of outer try catch");
}
catch (Exception)
{
x.GetHashCode();
}
}
}
public class MainClass
@ -124,11 +229,38 @@ public class MainClass
}
}
// 17 reports expected (18 with --pulse-increase-leak-recall flag)
// 18 reports expected (19 with --pulse-increase-leak-recall flag)
class InferResourceLeakTests
{
private static byte[] myBytes = new byte[] { 10, 4 };
/// <summary>
/// Validates that even though the Dispose occurs within a finally block, the exceptional
/// control flow is handled so that the analysis sees that the Dispose is not invoked. Also
/// gets coverage over finally blocks ending with throw instead of endfinally.
/// </summary>
public static void TryFinallyThrow()
{
var fs = new FileStream("", FileMode.Open);
var y = new FileStream("", FileMode.Open);
try
{
Console.Write("hello");
}
finally
{
if (fs != null)
{
fs.Dispose();
}
throw new Exception();
}
if (y != null)
{
y.Dispose();
}
}
/// <summary>
/// This is a false positive that occurs when we return a class that owns IDisposable types.
/// This occurs because the setter method at the end of the async MoveNext() method which
@ -428,6 +560,26 @@ class InferResourceLeakTests
return new TakeAndDisposeNotDisposable(stream);
}
/// <summary>
/// Should be no leak, as all resources are allocated via using.
/// </summary>
public static string NestedUsingWithThrownException()
{
using (var x = new StreamReader(""))
{
Console.Write("First using");
using (var y = new StreamReader(""))
{
Console.Write("Second using");
if (y != null)
{
throw new Exception();
}
}
}
return "done";
}
/// <summary>
/// Validates that upward casting of stream to IDisposable is not reported on.
/// </summary>