536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
783 lines
50 KiB
C#
783 lines
50 KiB
C#
namespace System.Web.Util {
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
using System.Security.Permissions;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Web;
|
|
|
|
internal static class AppVerifier {
|
|
|
|
// It's possible that multiple error codes might get mapped to the same description string.
|
|
// This can happen if there might be multiple ways for a single problem to manifest itself.
|
|
private static readonly Dictionary<AppVerifierErrorCode, string> _errorStringMappings = new Dictionary<AppVerifierErrorCode, string>() {
|
|
{ AppVerifierErrorCode.HttpApplicationInstanceWasNull, SR.AppVerifier_Errors_HttpApplicationInstanceWasNull },
|
|
{ AppVerifierErrorCode.BeginHandlerDelegateWasNull, SR.AppVerifier_Errors_BeginHandlerDelegateWasNull },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedMultipleTimes, SR.AppVerifier_Errors_AsyncCallbackInvokedMultipleTimes },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedWithNullParameter, SR.AppVerifier_Errors_AsyncCallbackInvokedWithNullParameter },
|
|
{ AppVerifierErrorCode.AsyncCallbackGivenAsyncResultWhichWasNotCompleted, SR.AppVerifier_Errors_AsyncCallbackGivenAsyncResultWhichWasNotCompleted },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyButAsyncResultWasNotMarkedCompletedSynchronously, SR.AppVerifier_Errors_AsyncCallbackInvokedSynchronouslyButAsyncResultWasNotMarkedCompletedSynchronously },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyButAsyncResultWasMarkedCompletedSynchronously, SR.AppVerifier_Errors_AsyncCallbackInvokedAsynchronouslyButAsyncResultWasMarkedCompletedSynchronously },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultInstance, SR.AppVerifier_Errors_AsyncCallbackInvokedWithUnexpectedAsyncResultInstance },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyThenBeginHandlerThrew, SR.AppVerifier_Errors_AsyncCallbackInvokedEvenThoughBeginHandlerThrew },
|
|
{ AppVerifierErrorCode.BeginHandlerThrewThenAsyncCallbackInvokedAsynchronously, SR.AppVerifier_Errors_AsyncCallbackInvokedEvenThoughBeginHandlerThrew },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyThenBeginHandlerThrew, SR.AppVerifier_Errors_AsyncCallbackInvokedEvenThoughBeginHandlerThrew },
|
|
{ AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultAsyncState, SR.AppVerifier_Errors_AsyncCallbackInvokedWithUnexpectedAsyncResultAsyncState },
|
|
{ AppVerifierErrorCode.AsyncCallbackCalledAfterHttpApplicationReassigned, SR.AppVerifier_Errors_AsyncCallbackCalledAfterHttpApplicationReassigned },
|
|
{ AppVerifierErrorCode.BeginHandlerReturnedNull, SR.AppVerifier_Errors_BeginHandlerReturnedNull },
|
|
{ AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButWhichWasNotCompleted, SR.AppVerifier_Errors_BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButWhichWasNotCompleted },
|
|
{ AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButAsyncCallbackNeverCalled, SR.AppVerifier_Errors_BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButAsyncCallbackNeverCalled },
|
|
{ AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultInstance, SR.AppVerifier_Errors_AsyncCallbackInvokedWithUnexpectedAsyncResultInstance },
|
|
{ AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultAsyncState, SR.AppVerifier_Errors_BeginHandlerReturnedUnexpectedAsyncResultAsyncState },
|
|
{ AppVerifierErrorCode.SyncContextSendOrPostCalledAfterRequestCompleted, SR.AppVerifier_Errors_SyncContextSendOrPostCalledAfterRequestCompleted },
|
|
{ AppVerifierErrorCode.SyncContextSendOrPostCalledBetweenNotifications, SR.AppVerifier_Errors_SyncContextSendOrPostCalledBetweenNotifications },
|
|
{ AppVerifierErrorCode.SyncContextPostCalledInNestedNotification, SR.AppVerifier_Errors_SyncContextPostCalledInNestedNotification },
|
|
{ AppVerifierErrorCode.RequestNotificationCompletedSynchronouslyWithNotificationContextPending, SR.AppVerifier_Errors_RequestNotificationCompletedSynchronouslyWithNotificationContextPending },
|
|
{ AppVerifierErrorCode.NotificationContextHasChangedAfterSynchronouslyProcessingNotification, SR.AppVerifier_Errors_NotificationContextHasChangedAfterSynchronouslyProcessingNotification },
|
|
{ AppVerifierErrorCode.PendingProcessRequestNotificationStatusAfterCompletingNestedNotification, SR.AppVerifier_Errors_PendingProcessRequestNotificationStatusAfterCompletingNestedNotification },
|
|
};
|
|
|
|
// Provides an option for different wrappers to specify whether to collect the call stacks traces
|
|
[FlagsAttribute]
|
|
internal enum CallStackCollectionBitMasks : int {
|
|
AllMask = -1,
|
|
|
|
// used for a 3-parameter Begin* method [(T, AsyncCallback, object) -> IAsyncResult] wrapper
|
|
BeginCallHandlerMask = 1,
|
|
CallHandlerCallbackMask = 2,
|
|
|
|
// used for a BeginEventHandler method [(object, sender, EventArgs, object) -> IAsyncResult] wrapper
|
|
BeginExecutionStepMask = 4,
|
|
ExecutionStepCallbackMask = 8,
|
|
|
|
// when adding new bits above also update the following:
|
|
AllHandlerMask = BeginCallHandlerMask | CallHandlerCallbackMask,
|
|
AllStepMask = BeginExecutionStepMask | ExecutionStepCallbackMask,
|
|
|
|
AllBeginMask = BeginCallHandlerMask | BeginExecutionStepMask,
|
|
AllCallbackMask = CallHandlerCallbackMask | ExecutionStepCallbackMask
|
|
};
|
|
|
|
// The declarative order of these two fields is important; don't swap them!
|
|
private static Action<AppVerifierException> DefaultAppVerifierBehavior = GetAppVerifierBehaviorFromRegistry();
|
|
internal static readonly bool IsAppVerifierEnabled = (DefaultAppVerifierBehavior != null);
|
|
private static long AppVerifierErrorCodeCollectCallStackMask;
|
|
private static long AppVerifierErrorCodeEnableAssertMask;
|
|
private static CallStackCollectionBitMasks AppVerifierCollectCallStackMask;
|
|
|
|
private delegate void AssertDelegate(bool condition, AppVerifierErrorCode errorCode);
|
|
private delegate void AppendAdditionalInfoDelegate(StringBuilder errorString);
|
|
|
|
// Returns an AppVerifier handler (something that can record exceptions appropriately)
|
|
// appropriate to what was set in the system registry. If the key we're looking for
|
|
// doesn't exist or doesn't have a known value, we return 'null', signifying that
|
|
// AppVerifier is disabled.
|
|
private static Action<AppVerifierException> GetAppVerifierBehaviorFromRegistry() {
|
|
// use 0 as the default value if the key doesn't exist or is of the wrong type
|
|
int valueFromRegistry = (Misc.GetAspNetRegValue(subKey: null, valueName: "RuntimeVerificationBehavior", defaultValue: null) as int?) ?? 0;
|
|
|
|
// REG_QWORD used as a mask to disable individual asserts. No key means all asserts are enabled
|
|
AppVerifierErrorCodeEnableAssertMask = (Misc.GetAspNetRegValue(subKey: null, valueName: "AppVerifierErrorCodeEnableAssertMask", defaultValue: (long)(-1)) as long?) ?? (long)(-1);
|
|
|
|
// REG_QWORD used as a mask to control call stack collection on individual asserts (useful if we event log only). No key means all asserts will collect stack traces
|
|
AppVerifierErrorCodeCollectCallStackMask = (Misc.GetAspNetRegValue(subKey: null, valueName: "AppVerifierErrorCodeCollectCallstackMask", defaultValue: (long)(-1)) as long?) ?? (long)(-1);
|
|
|
|
// REG_DWORD mask to disable call stack collection on begin* / end* methods. No key means all call stacks are collected
|
|
AppVerifierCollectCallStackMask = (CallStackCollectionBitMasks)((Misc.GetAspNetRegValue(subKey: null, valueName: "AppVerifierCollectCallStackMask", defaultValue: (int)(-1)) as int?) ?? (int)(-1));
|
|
|
|
switch (valueFromRegistry) {
|
|
case 1:
|
|
// Just write to the event log
|
|
return WriteToEventLog;
|
|
|
|
case 2:
|
|
// Write to the event log and Debugger.Launch / Debugger.Break
|
|
return WriteToEventLogAndSoftBreak;
|
|
|
|
case 3:
|
|
// Write to the event log and kernel32!DebugBreak
|
|
return WriteToEventLogAndHardBreak;
|
|
|
|
default:
|
|
// not enabled
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Writes an exception to the Windows Event Log (Application section)
|
|
private static void WriteToEventLog(AppVerifierException ex) {
|
|
Misc.WriteUnhandledExceptionToEventLog(AppDomain.CurrentDomain, ex); // method won't throw
|
|
}
|
|
|
|
[SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] // safe since AppVerifier can only be enabled via registry, which already requires admin privileges
|
|
private static void WriteToEventLogAndSoftBreak(AppVerifierException ex) {
|
|
// A "soft" break means that we prompt to launch a debugger, and if one is attached we'll signal it.
|
|
WriteToEventLog(ex);
|
|
if (Debugger.Launch()) {
|
|
Debugger.Break();
|
|
}
|
|
}
|
|
|
|
[SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] // safe since AppVerifier can only be enabled via registry, which already requires admin privileges
|
|
private static void WriteToEventLogAndHardBreak(AppVerifierException ex) {
|
|
// A "hard" break means that we'll signal any attached debugger, and if none is attached
|
|
// we'll just INT 3 and hope for the best. (This may cause a Watson dump depending on environment.)
|
|
WriteToEventLog(ex);
|
|
if (Debugger.IsAttached) {
|
|
Debugger.Break();
|
|
}
|
|
else {
|
|
NativeMethods.DebugBreak();
|
|
}
|
|
}
|
|
|
|
// Instruments a 3-parameter Begin* method [(T, AsyncCallback, object) -> IAsyncResult].
|
|
// If AppVerifier is disabled, returns the original method unmodified.
|
|
public static Func<T, AsyncCallback, object, IAsyncResult> WrapBeginMethod<T>(HttpApplication httpApplication, Func<T, AsyncCallback, object, IAsyncResult> originalDelegate) {
|
|
if (!IsAppVerifierEnabled) {
|
|
return originalDelegate;
|
|
}
|
|
|
|
return (arg, callback, state) => WrapBeginMethodImpl(
|
|
httpApplication: httpApplication,
|
|
beginMethod: (innerCallback, innerState) => originalDelegate(arg, innerCallback, innerState),
|
|
originalDelegate: originalDelegate,
|
|
errorHandler: HandleAppVerifierException,
|
|
callStackMask: CallStackCollectionBitMasks.AllHandlerMask)
|
|
(callback, state);
|
|
}
|
|
|
|
// Instruments a BeginEventHandler method [(object, sender, EventArgs, object) -> IAsyncResult].
|
|
// This pattern is commonly used, such as by IHttpModule, PageAsyncTask, and others.
|
|
// If AppVerifier is disabled, returns the original method unmodified.
|
|
public static BeginEventHandler WrapBeginMethod(HttpApplication httpApplication, BeginEventHandler originalDelegate) {
|
|
if (!IsAppVerifierEnabled) {
|
|
return originalDelegate;
|
|
}
|
|
|
|
return (sender, e, cb, extraData) => WrapBeginMethodImpl(
|
|
httpApplication: httpApplication,
|
|
beginMethod: (innerCallback, innerState) => originalDelegate(sender, e, innerCallback, innerState),
|
|
originalDelegate: originalDelegate,
|
|
errorHandler: HandleAppVerifierException,
|
|
callStackMask: CallStackCollectionBitMasks.AllStepMask)
|
|
(cb, extraData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps the Begin* part of a Begin / End method pair to allow for signaling when assertions have been violated.
|
|
/// The instrumentation can be a performance hit, so this method should not be called if AppVerifier is not enabled.
|
|
/// </summary>
|
|
/// <param name="httpApplication">The HttpApplication instance for this request, used to get HttpContext and related items.</param>
|
|
/// <param name="beginMethod">The Begin* part of a Begin / End method pair, likely wrapped in a lambda so only the AsyncCallback and object parameters are exposed.</param>
|
|
/// <param name="originalDelegate">The original user-provided delegate, e.g. the thing that 'beginMethod' wraps. Provided so that we can show correct methods when asserting.</param>
|
|
/// <param name="errorHandler">The listener that can handle verification failures.</param>
|
|
/// <returns>The instrumented Begin* method.</returns>
|
|
internal static Func<AsyncCallback, object, IAsyncResult> WrapBeginMethodImpl(HttpApplication httpApplication, Func<AsyncCallback, object, IAsyncResult> beginMethod, Delegate originalDelegate, Action<AppVerifierException> errorHandler, CallStackCollectionBitMasks callStackMask) {
|
|
return (callback, state) => {
|
|
// basic diagnostic info goes at the top since it's used during generation of the error message
|
|
AsyncCallbackInvocationHelper asyncCallbackInvocationHelper = new AsyncCallbackInvocationHelper();
|
|
CallStackCollectionBitMasks myBeginMask = callStackMask & CallStackCollectionBitMasks.AllBeginMask;
|
|
bool captureBeginStack = (myBeginMask & (CallStackCollectionBitMasks)AppVerifierCollectCallStackMask) == myBeginMask;
|
|
|
|
InvocationInfo beginHandlerInvocationInfo = InvocationInfo.Capture(captureBeginStack);
|
|
string requestUrl = null;
|
|
RequestNotification? currentNotification = null;
|
|
bool isPostNotification = false;
|
|
Type httpHandlerType = null;
|
|
|
|
// need to collect all this up-front since it might go away during the async operation
|
|
if (httpApplication != null) {
|
|
HttpContext context = httpApplication.Context;
|
|
if (context != null) {
|
|
if (!context.HideRequestResponse && context.Request != null) {
|
|
requestUrl = TryGetRequestUrl(context);
|
|
}
|
|
|
|
if (context.NotificationContext != null) {
|
|
currentNotification = context.NotificationContext.CurrentNotification;
|
|
isPostNotification = context.NotificationContext.IsPostNotification;
|
|
}
|
|
|
|
if (context.Handler != null) {
|
|
httpHandlerType = context.Handler.GetType();
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the condition passed to this method evaluates to false, we will raise an error to whoever is listening.
|
|
AssertDelegate assert = (condition, errorCode) => {
|
|
long mask = 1L<<(int)errorCode;
|
|
// assert only if it was not masked out by a bit set
|
|
bool enableAssert = (AppVerifierErrorCodeEnableAssertMask & mask) == mask;
|
|
|
|
if (!condition && enableAssert) {
|
|
// capture the stack only if it was not masked out by a bit set
|
|
bool captureStack = (AppVerifierErrorCodeCollectCallStackMask & mask) == mask;
|
|
|
|
InvocationInfo assertInvocationInfo = InvocationInfo.Capture(captureStack);
|
|
|
|
// header
|
|
StringBuilder errorString = new StringBuilder();
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_Title));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_Subtitle));
|
|
errorString.AppendLine();
|
|
|
|
// basic info (about the assert)
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_URL, requestUrl));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ErrorCode, (int)errorCode));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_Description, GetLocalizedDescriptionStringForError(errorCode)));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ThreadInfo, assertInvocationInfo.ThreadId, assertInvocationInfo.Timestamp.ToLocalTime()));
|
|
errorString.AppendLine(assertInvocationInfo.StackTrace.ToString());
|
|
|
|
// Begin* method info
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_EntryMethod, PrettyPrintDelegate(originalDelegate)));
|
|
if (currentNotification != null) {
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_RequestNotification_Integrated, currentNotification, isPostNotification));
|
|
}
|
|
else {
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_RequestNotification_NotIntegrated));
|
|
}
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_CurrentHandler, httpHandlerType));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_ThreadInfo, beginHandlerInvocationInfo.ThreadId, beginHandlerInvocationInfo.Timestamp.ToLocalTime()));
|
|
errorString.AppendLine(beginHandlerInvocationInfo.StackTrace.ToString());
|
|
|
|
// AsyncCallback info
|
|
int totalAsyncInvocationCount;
|
|
InvocationInfo firstAsyncInvocation = asyncCallbackInvocationHelper.GetFirstInvocationInfo(out totalAsyncInvocationCount);
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_AsyncCallbackInfo_InvocationCount, totalAsyncInvocationCount));
|
|
if (firstAsyncInvocation != null) {
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_AsyncCallbackInfo_FirstInvocation_ThreadInfo, firstAsyncInvocation.ThreadId, firstAsyncInvocation.Timestamp.ToLocalTime()));
|
|
errorString.AppendLine(firstAsyncInvocation.StackTrace.ToString());
|
|
}
|
|
|
|
AppVerifierException ex = new AppVerifierException(errorCode, errorString.ToString());
|
|
errorHandler(ex);
|
|
throw ex;
|
|
}
|
|
};
|
|
|
|
assert(httpApplication != null, AppVerifierErrorCode.HttpApplicationInstanceWasNull);
|
|
assert(originalDelegate != null, AppVerifierErrorCode.BeginHandlerDelegateWasNull);
|
|
|
|
object lockObj = new object(); // used to synchronize access to certain locals which can be touched by multiple threads
|
|
IAsyncResult asyncResultReturnedByBeginHandler = null;
|
|
IAsyncResult asyncResultPassedToCallback = null;
|
|
object beginHandlerReturnValueHolder = null; // used to hold the IAsyncResult returned by or Exception thrown by BeginHandler; see comments on Holder<T> for more info
|
|
Thread threadWhichCalledBeginHandler = Thread.CurrentThread; // used to determine whether the callback was invoked synchronously
|
|
bool callbackRanToCompletion = false; // don't need to lock when touching this local since it's only read in the synchronous case
|
|
|
|
HttpContext assignedContextUponCallingBeginHandler = httpApplication.Context; // used to determine whether the underlying request disappeared
|
|
|
|
try {
|
|
asyncResultReturnedByBeginHandler = beginMethod(
|
|
asyncResult => {
|
|
try {
|
|
CallStackCollectionBitMasks myCallbackMask = callStackMask & CallStackCollectionBitMasks.AllCallbackMask;
|
|
bool captureEndCallStack = (myCallbackMask & AppVerifierCollectCallStackMask ) == myCallbackMask;
|
|
// The callback must never be called more than once.
|
|
int newAsyncCallbackInvocationCount = asyncCallbackInvocationHelper.RecordInvocation(captureEndCallStack);
|
|
assert(newAsyncCallbackInvocationCount == 1, AppVerifierErrorCode.AsyncCallbackInvokedMultipleTimes);
|
|
|
|
// The 'asyncResult' parameter must never be null.
|
|
assert(asyncResult != null, AppVerifierErrorCode.AsyncCallbackInvokedWithNullParameter);
|
|
|
|
object tempBeginHandlerReturnValueHolder;
|
|
Thread tempThreadWhichCalledBeginHandler;
|
|
lock (lockObj) {
|
|
asyncResultPassedToCallback = asyncResult;
|
|
tempBeginHandlerReturnValueHolder = beginHandlerReturnValueHolder;
|
|
tempThreadWhichCalledBeginHandler = threadWhichCalledBeginHandler;
|
|
}
|
|
|
|
// At this point, 'IsCompleted = true' is mandatory.
|
|
assert(asyncResult.IsCompleted, AppVerifierErrorCode.AsyncCallbackGivenAsyncResultWhichWasNotCompleted);
|
|
|
|
if (tempBeginHandlerReturnValueHolder == null) {
|
|
// BeginHandler hasn't yet returned, so this call may be synchronous or asynchronous.
|
|
// We can tell by comparing the current thread with the thread which called BeginHandler.
|
|
// From a correctness perspective, it is valid to invoke the AsyncCallback delegate either
|
|
// synchronously or asynchronously. From Microsoft: if 'CompletedSynchronously = true', then
|
|
// AsyncCallback invocation can happen either on the same thread or on a different thread,
|
|
// just as long as BeginHandler hasn't yet returned (which in true in this case).
|
|
if (!asyncResult.CompletedSynchronously) {
|
|
// If 'CompletedSynchronously = false', we must be on a different thread than the BeginHandler invocation.
|
|
assert(tempThreadWhichCalledBeginHandler != Thread.CurrentThread, AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyButAsyncResultWasNotMarkedCompletedSynchronously);
|
|
}
|
|
}
|
|
else {
|
|
// BeginHandler already returned, so this invocation is definitely asynchronous.
|
|
|
|
Holder<IAsyncResult> asyncResultHolder = tempBeginHandlerReturnValueHolder as Holder<IAsyncResult>;
|
|
if (asyncResultHolder != null) {
|
|
// We need to verify that the IAsyncResult we're given is the same that was returned by BeginHandler
|
|
// and that the IAsyncResult is marked 'CompletedSynchronously = false'.
|
|
assert(asyncResult == asyncResultHolder.Value, AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultInstance);
|
|
assert(!asyncResult.CompletedSynchronously, AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyButAsyncResultWasMarkedCompletedSynchronously);
|
|
}
|
|
else {
|
|
// If we reached this point, BeginHandler threw an exception.
|
|
// The AsyncCallback should never be invoked if BeginHandler has already failed.
|
|
assert(false, AppVerifierErrorCode.BeginHandlerThrewThenAsyncCallbackInvokedAsynchronously);
|
|
}
|
|
}
|
|
|
|
// AsyncState must match the 'state' parameter passed to BeginHandler
|
|
assert(asyncResult.AsyncState == state, AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultAsyncState);
|
|
|
|
// Make sure the underlying HttpApplication is still assigned to the captured HttpContext instance.
|
|
// If not, this AsyncCallback invocation could end up completing *some other request's* operation,
|
|
// resulting in data corruption.
|
|
assert(assignedContextUponCallingBeginHandler == httpApplication.Context, AppVerifierErrorCode.AsyncCallbackCalledAfterHttpApplicationReassigned);
|
|
}
|
|
catch (AppVerifierException) {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener. Just go straight to
|
|
// invoking the callback.
|
|
}
|
|
|
|
// all checks complete - delegate control to the actual callback
|
|
if (callback != null) {
|
|
callback(asyncResult);
|
|
}
|
|
callbackRanToCompletion = true;
|
|
},
|
|
state);
|
|
|
|
// The return value must never be null.
|
|
assert(asyncResultReturnedByBeginHandler != null, AppVerifierErrorCode.BeginHandlerReturnedNull);
|
|
|
|
lock (lockObj) {
|
|
beginHandlerReturnValueHolder = new Holder<IAsyncResult>(asyncResultReturnedByBeginHandler);
|
|
}
|
|
|
|
if (asyncResultReturnedByBeginHandler.CompletedSynchronously) {
|
|
// If 'CompletedSynchronously = true', the IAsyncResult must be marked 'IsCompleted = true'
|
|
// and the AsyncCallback must have been invoked synchronously (checked in the AsyncCallback verification logic).
|
|
assert(asyncResultReturnedByBeginHandler.IsCompleted, AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButWhichWasNotCompleted);
|
|
assert(asyncCallbackInvocationHelper.TotalInvocations != 0, AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButAsyncCallbackNeverCalled);
|
|
}
|
|
|
|
IAsyncResult tempAsyncResultPassedToCallback;
|
|
lock (lockObj) {
|
|
tempAsyncResultPassedToCallback = asyncResultPassedToCallback;
|
|
}
|
|
|
|
// The AsyncCallback may have been invoked (either synchronously or asynchronously). If it has been
|
|
// invoked, we need to verify that it was given the same IAsyncResult returned by BeginHandler.
|
|
// If the AsyncCallback hasn't yet been called, we skip this check, as the AsyncCallback verification
|
|
// logic will eventually perform the check at the appropriate time.
|
|
if (tempAsyncResultPassedToCallback != null) {
|
|
assert(tempAsyncResultPassedToCallback == asyncResultReturnedByBeginHandler, AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultInstance);
|
|
}
|
|
|
|
// AsyncState must match the 'state' parameter passed to BeginHandler
|
|
assert(asyncResultReturnedByBeginHandler.AsyncState == state, AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultAsyncState);
|
|
|
|
// all checks complete
|
|
return asyncResultReturnedByBeginHandler;
|
|
}
|
|
catch (AppVerifierException) {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener. Just return the original
|
|
// IAsyncResult so that the application continues to run.
|
|
return asyncResultReturnedByBeginHandler;
|
|
}
|
|
catch (Exception ex) {
|
|
if (asyncResultReturnedByBeginHandler == null) {
|
|
// If we reached this point, an exception was thrown by BeginHandler, so we need to
|
|
// record it and rethrow it.
|
|
|
|
IAsyncResult tempAsyncResultPassedToCallback;
|
|
lock (lockObj) {
|
|
beginHandlerReturnValueHolder = new Holder<Exception>(ex);
|
|
tempAsyncResultPassedToCallback = asyncResultPassedToCallback;
|
|
}
|
|
|
|
try {
|
|
// The AsyncCallback should only be invoked if BeginHandler ran to completion.
|
|
if (tempAsyncResultPassedToCallback != null) {
|
|
|
|
// If AsyncCallback was invoked asynchronously, then by definition it was
|
|
// scheduled prematurely since BeginHandler hadn't yet run to completion
|
|
// (since whatever additional work it did after invoking the callback failed).
|
|
// Therefore it is always wrong for BeginHandler to both throw and
|
|
// asynchronously invoke AsyncCallback.
|
|
assert(tempAsyncResultPassedToCallback.CompletedSynchronously, AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyThenBeginHandlerThrew);
|
|
|
|
// If AsyncCallback was invoked synchronously, then it must have been invoked
|
|
// before BeginHandler surfaced the exception (since otherwise BeginHandler
|
|
// wouldn't have reached the line of code that invoked AsyncCallback). But
|
|
// AsyncCallback itself could have thrown, bubbling the exception up through
|
|
// BeginHandler and back to us. If AsyncCallback ran to completion, then this
|
|
// means BeginHandler did extra work (which failed) after invoking AsyncCallback,
|
|
// so BeginHandler by definition hadn't yet run to completion.
|
|
assert(!callbackRanToCompletion, AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyThenBeginHandlerThrew);
|
|
}
|
|
}
|
|
catch (AppVerifierException) {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener. Propagate the original
|
|
// exception upward.
|
|
}
|
|
|
|
throw;
|
|
}
|
|
else {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener. Just return the original
|
|
// IAsyncResult so that the application continues to run.
|
|
return asyncResultReturnedByBeginHandler;
|
|
}
|
|
}
|
|
finally {
|
|
// Since our local variables are GC-rooted in an anonymous object, we should
|
|
// clear references to objects we no longer need so that the GC can reclaim
|
|
// them if appropriate.
|
|
lock (lockObj) {
|
|
threadWhichCalledBeginHandler = null;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Gets a delegate that checks for application code trying to call into the SyncContext after
|
|
// the request or the request notification is already completed.
|
|
// The Action returned by this method could be null.
|
|
public static Action<bool> GetSyncContextCheckDelegate(ISyncContext syncContext) {
|
|
if (!IsAppVerifierEnabled) {
|
|
return null;
|
|
}
|
|
|
|
return GetSyncContextCheckDelegateImpl(syncContext, HandleAppVerifierException);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an Action<bool> that determines whether SynchronizationContext.Send or Post was called after the underlying request
|
|
/// or the request notification finished. The bool parameter controls whether to check if Post is attempted in nested notification.
|
|
/// The instrumentation can be a performance hit, so this method should not be called if AppVerifier is not enabled.
|
|
/// </summary>
|
|
/// <param name="syncContext">The ISyncContext (HttpApplication, WebSocketPipeline, etc.) on which to perform the check.</param>
|
|
/// <param name="errorHandler">The listener that can handle verification failures.</param>
|
|
/// <returns>A callback which performs the verification.</returns>
|
|
internal static Action<bool> GetSyncContextCheckDelegateImpl(ISyncContext syncContext, Action<AppVerifierException> errorHandler) {
|
|
string requestUrl = null;
|
|
object originalThreadContextId = null;
|
|
|
|
// collect all of the diagnostic information upfront
|
|
HttpContext originalHttpContext = (syncContext != null) ? syncContext.HttpContext : null;
|
|
if (originalHttpContext != null) {
|
|
if (!originalHttpContext.HideRequestResponse && originalHttpContext.Request != null) {
|
|
requestUrl = TryGetRequestUrl(originalHttpContext);
|
|
}
|
|
|
|
// This will be used as a surrogate for the captured HttpContext so that we don't
|
|
// have a long-lived reference to a heavy object graph. See comments on ThreadContextId
|
|
// for more info.
|
|
originalThreadContextId = originalHttpContext.ThreadContextId;
|
|
originalHttpContext = null;
|
|
}
|
|
|
|
// If the condition passed to this method evaluates to false, we will raise an error to whoever is listening.
|
|
AssertDelegate assert = GetAssertDelegateImpl(requestUrl, errorHandler, appendAdditionalInfoDelegate: null);
|
|
|
|
return (checkForReEntry) => {
|
|
try {
|
|
// Make sure that the ISyncContext is still associated with the same HttpContext that
|
|
// we captured earlier.
|
|
HttpContext currentHttpContext = (syncContext != null) ? syncContext.HttpContext : null;
|
|
object currentThreadContextId = (currentHttpContext != null) ? currentHttpContext.ThreadContextId : null;
|
|
assert(currentThreadContextId != null && ReferenceEquals(originalThreadContextId, currentThreadContextId), AppVerifierErrorCode.SyncContextSendOrPostCalledAfterRequestCompleted);
|
|
|
|
if (HttpRuntime.UsingIntegratedPipeline && !currentHttpContext.HasWebSocketRequestTransitionCompleted) {
|
|
var notificationContext = (currentHttpContext != null) ? currentHttpContext.NotificationContext : null;
|
|
assert(notificationContext != null, AppVerifierErrorCode.SyncContextSendOrPostCalledBetweenNotifications);
|
|
|
|
if (checkForReEntry && notificationContext != null) {
|
|
assert(!notificationContext.IsReEntry, AppVerifierErrorCode.SyncContextPostCalledInNestedNotification);
|
|
}
|
|
}
|
|
}
|
|
catch (AppVerifierException) {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener. Propagate the original
|
|
// exception upward.
|
|
}
|
|
};
|
|
}
|
|
|
|
// This generic method invokes a delegate that was created by AppVerifier at an earlier time.
|
|
// It is safe to call it even if the returned delegate is null (e.g. AppVerifier is off).
|
|
// Here is the typical usage scenario:
|
|
// var verifierCheck = AppVerifier.Get*CheckDelegate(...); // get the delegate which can capture some state
|
|
// T result = <...> // the result of some code execution
|
|
// AppVerifier.InvokeVerifierCheck(verifierCheckDelegate, result); // invoke the verification of the result
|
|
internal static void InvokeVerifierCheck<T>(Action<T> verifierCheckDelegate, T result)
|
|
{
|
|
if (verifierCheckDelegate != null) {
|
|
try {
|
|
verifierCheckDelegate(result);
|
|
}
|
|
catch (AppVerifierException) {
|
|
// We want to ---- any exceptions thrown by our verification logic, as the failure
|
|
// has already been recorded by the appropriate listener.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gets a delegate that checks for inconsistencies after managed code finished processing one or more request notifications.
|
|
// The Action returned by this method could be null.
|
|
internal static Action<RequestNotificationStatus> GetRequestNotificationStatusCheckDelegate(HttpContext context, RequestNotification currentNotification, bool isPostNotification) {
|
|
if (!IsAppVerifierEnabled) {
|
|
return null;
|
|
}
|
|
return GetRequestNotificationStatusCheckDelegateImpl(context, currentNotification, isPostNotification, HandleAppVerifierException);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an Action<RequestNotificationStatus> that will check for inconsistencies after
|
|
/// managed code has finished processing one or more notifications and about to return back to IIS.
|
|
/// </summary>
|
|
/// <returns>A callback which performs the verification.</returns>
|
|
internal static Action<RequestNotificationStatus> GetRequestNotificationStatusCheckDelegateImpl(HttpContext context, RequestNotification currentNotification, bool isPostNotification, Action<AppVerifierException> errorHandler) {
|
|
// collect all of the diagnostic information upfront
|
|
NotificationContext originalNotificationContext = context.NotificationContext;
|
|
bool isReentry = originalNotificationContext.IsReEntry;
|
|
string requestUrl = null;
|
|
if (!context.HideRequestResponse && context.Request != null) {
|
|
requestUrl = TryGetRequestUrl(context);
|
|
}
|
|
|
|
AppendAdditionalInfoDelegate appendCurrentNotificationInfo = (errorString) => {
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_NotificationInfo, currentNotification, isPostNotification, isReentry));
|
|
};
|
|
|
|
AssertDelegate assert = GetAssertDelegateImpl(requestUrl, errorHandler, appendAdditionalInfoDelegate: appendCurrentNotificationInfo);
|
|
|
|
return (RequestNotificationStatus status) => {
|
|
if (status == RequestNotificationStatus.Pending) {
|
|
// We don't expect nested notifications to complete asynchronously
|
|
assert(!isReentry, AppVerifierErrorCode.PendingProcessRequestNotificationStatusAfterCompletingNestedNotification);
|
|
}
|
|
else {
|
|
// Completing synchronously with pending NotificationContext means a bug in either user code or the pipeline.
|
|
// NotificationContext being null means we already completed asynchronously before completing synchronously.
|
|
// Both cases indicate that we have some async operations we failed to account for.
|
|
assert(context.NotificationContext != null && !context.NotificationContext.PendingAsyncCompletion,
|
|
AppVerifierErrorCode.RequestNotificationCompletedSynchronouslyWithNotificationContextPending);
|
|
|
|
// Can't have a different NotificationContext after finishing the notification
|
|
// Even if it was changed while processing nested notifications it should be restored back before we unwind
|
|
assert(context.NotificationContext == originalNotificationContext,
|
|
AppVerifierErrorCode.NotificationContextHasChangedAfterSynchronouslyProcessingNotification);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method returns the default implementation of the assert code which takes care of
|
|
/// evaluating the assert contition, handing assert and stack collection enabling masks,
|
|
/// and creating an AppVerifierException with basic information
|
|
/// </summary>
|
|
/// <param name="requestUrl">The Url of the request.</param>
|
|
/// <param name="errorHandler">The listener that can handle verification failures.</param>
|
|
/// <param name="appendAdditionalInfoDelegate">The caller can provide this delegate to append additional information to the exception. Could be null.</param>
|
|
/// <returns>A callback which performs the verification.</returns>
|
|
private static AssertDelegate GetAssertDelegateImpl(string requestUrl, Action<AppVerifierException> errorHandler, AppendAdditionalInfoDelegate appendAdditionalInfoDelegate) {
|
|
// If the condition passed to this method evaluates to false, we will raise an error to whoever is listening.
|
|
return (condition, errorCode) => {
|
|
long mask = 1L << (int)errorCode;
|
|
// assert only if it was not masked out by a bit set
|
|
bool enableAssert = (AppVerifierErrorCodeEnableAssertMask & mask) == mask;
|
|
|
|
if (!condition && enableAssert) {
|
|
// capture the stack only if it was not masked out by a bit set
|
|
bool captureStack = (AppVerifierErrorCodeCollectCallStackMask & mask) == mask;
|
|
InvocationInfo assertInvocationInfo = InvocationInfo.Capture(captureStack);
|
|
|
|
// header
|
|
StringBuilder errorString = new StringBuilder();
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_Title));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_Subtitle));
|
|
errorString.AppendLine();
|
|
|
|
// basic info (about the assert)
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_URL, requestUrl));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ErrorCode, (int)errorCode));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_Description, GetLocalizedDescriptionStringForError(errorCode)));
|
|
errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ThreadInfo, assertInvocationInfo.ThreadId, assertInvocationInfo.Timestamp.ToLocalTime()));
|
|
|
|
// append additional info if needed
|
|
if (appendAdditionalInfoDelegate != null) {
|
|
appendAdditionalInfoDelegate(errorString);
|
|
}
|
|
|
|
// append the stack trace
|
|
errorString.AppendLine(assertInvocationInfo.StackTrace.ToString());
|
|
|
|
AppVerifierException ex = new AppVerifierException(errorCode, errorString.ToString());
|
|
errorHandler(ex);
|
|
throw ex;
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
// This is the default implementation of an AppVerifierException handler;
|
|
// it just delegates to the configured behavior.
|
|
[SuppressMessage("Microsoft.Reliability", "CA2004:RemoveCallsToGCKeepAlive", Justification = "Want to keep these locals on the stack to assist with debugging.")]
|
|
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
|
|
private static void HandleAppVerifierException(AppVerifierException ex) {
|
|
// This method is specifically written to maximize the chance of
|
|
// useful information being on the stack as a local, where it's more
|
|
// easily observed by the debugger.
|
|
|
|
AppVerifierErrorCode errorCode = ex.ErrorCode;
|
|
string fullMessage = ex.Message;
|
|
|
|
DefaultAppVerifierBehavior(ex);
|
|
|
|
GC.KeepAlive(errorCode);
|
|
GC.KeepAlive(fullMessage);
|
|
GC.KeepAlive(ex);
|
|
}
|
|
|
|
private static string TryGetRequestUrl(HttpContext context) {
|
|
try {
|
|
return context.Request.EnsureRawUrl();
|
|
}
|
|
catch (HttpException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal static string PrettyPrintDelegate(Delegate del) {
|
|
return PrettyPrintMemberInfo((del != null) ? del.Method : null);
|
|
}
|
|
|
|
// prints "TResult MethodName(TArg1, TArg2, ...) [Module.dll!Namespace.TypeName]"
|
|
internal static string PrettyPrintMemberInfo(MethodInfo method) {
|
|
if (method == null) {
|
|
return null;
|
|
}
|
|
|
|
string retVal = method.ToString();
|
|
|
|
Type type = method.ReflectedType;
|
|
if (type != null) {
|
|
retVal = retVal + " [";
|
|
if (type.Module != null) {
|
|
retVal += type.Module.Name + "!";
|
|
}
|
|
|
|
retVal += type.FullName + "]";
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
|
|
internal static string GetLocalizedDescriptionStringForError(AppVerifierErrorCode errorCode) {
|
|
return FormatErrorString(_errorStringMappings[errorCode]);
|
|
}
|
|
|
|
// We use InstalledUICulture rather than CurrentCulture / CurrentUICulture since these strings will
|
|
// be stored in the system event log.
|
|
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.IFormatProvider,System.String,System.Object[])",
|
|
Justification = "Matches culture specified in Misc.WriteUnhandledExceptionToEventLog.")]
|
|
internal static string FormatErrorString(string name, params object[] args) {
|
|
return String.Format(CultureInfo.InstalledUICulture, SR.Resources.GetString(name, CultureInfo.InstalledUICulture), args);
|
|
}
|
|
|
|
// contains a counter and invocation information for an AsyncCallback delegate
|
|
private sealed class AsyncCallbackInvocationHelper {
|
|
private InvocationInfo _firstInvocationInfo;
|
|
private int _totalInvocationCount;
|
|
|
|
public int TotalInvocations {
|
|
[MethodImpl(MethodImplOptions.Synchronized)]
|
|
get { return _totalInvocationCount; }
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.Synchronized)]
|
|
public InvocationInfo GetFirstInvocationInfo(out int totalInvocationCount) {
|
|
totalInvocationCount = _totalInvocationCount;
|
|
return _firstInvocationInfo;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.Synchronized)]
|
|
public int RecordInvocation(bool captureCallStack) {
|
|
_totalInvocationCount++;
|
|
if (_firstInvocationInfo == null) {
|
|
_firstInvocationInfo = InvocationInfo.Capture(captureCallStack);
|
|
}
|
|
return _totalInvocationCount;
|
|
}
|
|
}
|
|
|
|
// We use a special class for holding data so that we can store the local's
|
|
// intended type alongside its real value. Prevents us from misinterpreting
|
|
// the degenerate case of "----CustomType : Exception, IAsyncResult" so that
|
|
// we know whether it was returned as an IAsyncResult or thrown as an Exception.
|
|
private sealed class Holder<T> {
|
|
public readonly T Value;
|
|
|
|
public Holder(T value) {
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
// holds diagnostic information about a particular invocation
|
|
private sealed class InvocationInfo {
|
|
public readonly int ThreadId;
|
|
public readonly DateTimeOffset Timestamp;
|
|
public readonly string StackTrace;
|
|
|
|
private InvocationInfo(bool captureStack) {
|
|
ThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
Timestamp = DateTimeOffset.UtcNow; // UTC is faster, will convert to local on error
|
|
StackTrace = captureStack? CaptureStackTrace(): "n/a";
|
|
}
|
|
|
|
public static InvocationInfo Capture(bool captureStack) {
|
|
return new InvocationInfo(captureStack);
|
|
}
|
|
|
|
// captures a stack trace, removing AppVerifier.* frames from the top of the stack to minimize noise
|
|
private static string CaptureStackTrace() {
|
|
StackTrace fullStackTrace = new StackTrace(fNeedFileInfo: true);
|
|
string[] traceLines = fullStackTrace.ToString().Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
|
|
for (int i = 0; i < fullStackTrace.FrameCount && i < traceLines.Length; i++) {
|
|
StackFrame thisFrame = fullStackTrace.GetFrame(i);
|
|
if (thisFrame.GetMethod().Module == typeof(AppVerifier).Module
|
|
&& thisFrame.GetMethod().DeclaringType.FullName.StartsWith("System.Web.Util.AppVerifier", StringComparison.Ordinal)) {
|
|
// we want to skip this frame since it's an AppVerifier.* frame
|
|
continue;
|
|
}
|
|
else {
|
|
// this is the first frame that is not an AppVerifier.* frame, so start the stack trace from here
|
|
return String.Join(Environment.NewLine, traceLines.Skip(i));
|
|
}
|
|
}
|
|
|
|
// if we reached this point, not sure what happened, so just return the original stack trace
|
|
return fullStackTrace.ToString();
|
|
}
|
|
}
|
|
|
|
[SuppressUnmanagedCodeSecurityAttribute]
|
|
private static class NativeMethods {
|
|
[DllImport("kernel32.dll")]
|
|
internal extern static void DebugBreak();
|
|
}
|
|
|
|
}
|
|
}
|