I'm attaching a patch for a new rule:
DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule in
Gendarme.Rules.Interoperability.
Since I didn't read the faq properly, I already committed the rule to
svn, I'm really sorry about this.
The documentation in mono-project.com/Gendarme.Rules.Interoperability
has also been written.
Feedback welcome :)
Rolf
I like the idea behind this rule a lot. I wonder though, if it should
be a more general rule. For example, unhandled exceptions in thread
callbacks are nearly as evil as unhandled exceptions in native
delegates (see <http://msdn.microsoft.com/en-us/library/ms228965.aspx>).
More comments inline...
> Index: DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
> ===================================================================
> --- DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
> (revision 0)
> +++ DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
> (revision 125705)
> @@ -0,0 +1,458 @@
> +//
> +//
> Gendarme
> .Rules
> .Interoperability
> .DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule
> +//
> +// Authors:
> +// Rolf Bjarne Kvinge <RKv...@novell.com>
> +//
> +// Copyright (C) 2009 Novell, Inc (http://www.novell.com)
> +//
> +// Permission is hereby granted, free of charge, to any person
> obtaining
> +// a copy of this software and associated documentation files (the
> +// "Software"), to deal in the Software without restriction,
> including
> +// without limitation the rights to use, copy, modify, merge,
> publish,
> +// distribute, sublicense, and/or sell copies of the Software, and to
> +// permit persons to whom the Software is furnished to do so,
> subject to
> +// the following conditions:
> +//
> +// The above copyright notice and this permission notice shall be
> +// included in all copies or substantial portions of the Software.
> +//
> +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
> +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
> +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
> HOLDERS BE
> +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
> ACTION
> +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
> CONNECTION
> +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> +//
> +
> +using System;
> +using System.Collections.Generic;
> +
> +using Mono.Cecil;
> +using Mono.Cecil.Cil;
> +
> +using Gendarme.Framework;
> +using Gendarme.Framework.Engines;
> +using Gendarme.Framework.Helpers;
> +using Gendarme.Framework.Rocks;
> +
> +namespace Gendarme.Rules.Interoperability {
> +
> + /// <summary>
> + /// <code>Every delegate which is passed to native code must
> include an exception block which spans the entire method and has a
> catch clause with no condition.</code>
This language seems a trifle clumsy. Something like "and has a catch
all block." would be clearer I think.
>
> + /// </summary>
> + /// <example>
> + /// Bad example:
> + /// <code>
> + /// public void NativeCallback ()
> + /// {
> + /// Console.WriteLine ("{0}", 1);
> + /// }
> + /// </code>
> + /// </example>
> + /// <example>
> + /// Good example:
> + /// <code>
> + /// public void NativeCallback ()
> + /// {
> + /// try {
> + /// Console.WriteLine ("{0}", 1);
> + /// } catch {
> + /// }
> + /// }
>
> + /// </code>
> + /// </example>
> +
> + [Problem ("Every delegate passed to native code must include an
> exception block which spans the entire method and has a catch clause
> with no condition.")]
> + [Solution ("Surround the entire method body with a try/catch
> block.")]
> + [EngineDependency (typeof (OpCodeEngine))]
> + public class
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule : Rule,
> IMethodRule {
Should probably seal the class.
>
> + /// <summary>
> + /// A list of methods which have been verified to be safe to call
> from native code.
> + /// </summary>
> + Dictionary<MethodDefinition, bool> verified_methods;
> +
> + /// <summary>
> + /// A list of methods which have been reported to be unsafe (to
> not report the same method twice).
> + /// </summary>
> + List<MethodDefinition> reported_methods;
> +
> + /// <summary>
> + /// A list of all the fields which have been passed to pinvokes
> (as a delegate parameter).
> + /// We report an error if a ld[s]fld stores an unsafe function
> pointer into any of these fields.
> + /// </summary>
> + List<FieldDefinition> fields_loads;
> +
> + /// <summary>
> + /// A list of all the fields which have been assigned function
> pointers.
> + /// We report an error if a pinvoke loads any of these fields.
> + /// </summary>
> + Dictionary<FieldDefinition, List<MethodDefinition>> field_stores;
> +
> + public RuleResult CheckMethod (MethodDefinition method)
> + {
> + MethodDefinition called_method;
> + List<MethodDefinition> pointers;
> + List<List<MethodDefinition>> locals = new
> List<List<MethodDefinition>> ();
> + List<ILRange> stack = new List<ILRange> (); // A rude way of
> assigning code sequences to stack positions.
> +
> + // Rule does not apply if the method has no IL
> + if (!method.HasBody)
> + return RuleResult.DoesNotApply;
Do this first thing so allocations can be skipped. It should also be
possible to use OpCodeBitmask to prune more methods.
>
>
> +
> + // Console.WriteLine ("Checking method: {0} on type: {1}",
> method.Name, method.DeclaringType.FullName);
It's better to use the logger, especially for complex methods like
this. The logger will allow you to dump the disassembled methods which
is very handy and allows people to easily debug your rule without
going in and manually un-commenting the CWLs.
>
> +
> + //
> + // We check the following code patterns:
> + // - All delegate expressions created and passed directly to the
> pinvoke
> + // - All delegate expressions created and stored in fields or
> local variables and then passed to the pinvoke.
> + //
> + // To do the second item, we keep track of all reads from fields
> to pinvoke and stores to fields (with delegates)
> + // and report an error if the delegate is unsafe in either case
> (i.e. we read from a field which has been
> + // assigned an unsafe delegate, or we store an unsafe delegate
> in a field which have been used in a pinvoke).
> + // This way only one pass over the code is required.
> + //
> + // No other static analysis is done, which means that for things
> like this we'll report false positives:
> + // delegate v = SafeMethod;
> + // PInvoke (v);
> + // v = UnsafeMethod;
> + //
> + //
> + // There are several ways to get around this:
> + // - Create a delegate, store it in a array/list and then pass
> it to a pinvoke.
> + // - Create a delegate, return it from a function and use the
> return value directly in a call to a pinvoke.
> + // - ...
> + //
> +
> + int stack_count = 0;
> + foreach (Instruction ins in method.Body.Instructions) {
> + int push = ins.GetPushCount ();
> + int pop = ins.GetPopCount (method);
> +
> + // Console.WriteLine ("before {0} + push {1} - pop {2} = after
> {3} {4}", stack_count, push, pop, stack_count + push - pop, ToString
> (ins));
> + // Console.WriteLine ("{4}", stack_count, push, pop,
> stack_count + push - pop, ToString (ins));
> +
> + if (ins.OpCode.Code == Code.Call) {
> + called_method = (ins.Operand as MethodReference).Resolve ();
> +
> + if (called_method != null && called_method.IsPInvokeImpl) {
> + // Console.WriteLine ("Reached a call instruction to a
> pinvoke method: {0}", called_method.Name);
> +
> + for (int i = 0; i < called_method.Parameters.Count; i++) {
> + if (!called_method.Parameters [i].ParameterType.IsDelegate ())
> + continue;
> +
> + // Console.WriteLine (" Stack slot #{0}:", i);
> + // stack [i].DumpAll (" ");
> +
> + // if we load a field, store the field so that any
> subsequent unsafe writes to that field can be reported.
> + Instruction last = stack [i].Last;
> + if (last.OpCode.Code == Code.Ldfld || last.OpCode.Code ==
> Code.Ldsfld) {
> + FieldDefinition field = (last.Operand as
> FieldReference).Resolve ();
> + if (fields_loads == null)
> + fields_loads = new List<FieldDefinition> ();
I'm not so sure it's an optimization to constantly perform these null
checks. Probably better to just allocate them up front and clear them
on rule entry.
>
> + if (!fields_loads.Contains (field))
> + fields_loads.Add (field);
> + }
> +
> + // Get and check the pointers
> + VerifyMethods (GetDelegatePointers (method, locals, stack
> [i]));
> + }
> + }
> + } else if (ins.OpCode.Code == Code.Stfld || ins.OpCode.Code ==
> Code.Stsfld) {
> + FieldDefinition field = (ins.Operand as
> FieldReference).Resolve ();
> +
> + pointers = GetDelegatePointers (method, locals, stack
> [stack_count - 1]);
> +
> + // Console.WriteLine (" Reached a field variable store to the
> field {0}, there are {1} unsafe pointers here.", field.Name,
> pointers == null ? 0 : pointers.Count);
> +
> + if (pointers != null && pointers.Count > 0) {
> + List<MethodDefinition> tmp;
> + if (field_stores == null) {
> + field_stores = new Dictionary<FieldDefinition,
> List<MethodDefinition>> ();
> + tmp = new List<MethodDefinition> ();
> + field_stores.Add (field, tmp);
> + } else if (!field_stores.TryGetValue (field, out tmp)) {
> + tmp = new List<MethodDefinition> ();
> + field_stores.Add (field, tmp);
> + }
> + tmp.AddRange (pointers);
> + }
> +
> + // If this field has been loaded into a pinvoke,
> + // check the list for unsafe pointers.
> + if (fields_loads != null && fields_loads.Contains (field))
> + VerifyMethods (pointers);
> + } else if (ins.IsStoreLocal ()) {
> + int index = ins.GetStoreIndex ();
> +
> + pointers = GetDelegatePointers (method, locals, stack
> [stack_count - 1]);
> +
> + // Console.WriteLine (" Reached a local variable store at
> index {0}, there are {1} pointers here.", index, pointers == null ?
> 0 : pointers.Count);
> +
> + if (pointers != null && pointers.Count > 0) {
> + while (locals.Count <= index)
> + locals.Add (null);
> + if (locals [index] == null)
> + locals [index] = new List<MethodDefinition> ();
> + locals [index].AddRange (pointers);
> + }
> + }
> +
> + stack_count += push - pop;
> +
> + while (stack_count > stack.Count)
> + stack.Add (new ILRange (ins));
> + while (stack_count < stack.Count)
> + stack.RemoveAt (stack.Count - 1);
> +
> + for (int i = 0; i < stack_count; i++)
> + stack [i].Last = ins;
> + }
> +
> + // Console.WriteLine ("Checking method: {0} [Done]", method.Name);
> +
> + return Runner.CurrentRuleResult;
> + }
> +
> + /// <summary>
> + /// Verifies that the method is safe to call as a callback from
> native code.
> + /// </summary>
> + private bool VerifyCallbackSafety (MethodDefinition callback)
> + {
> + bool result;
> + bool valid_ex_handler = false;
> +
> + if (callback == null)
> + return true;
> +
> + if (!callback.HasBody)
> + return true;
> +
> + if (verified_methods != null && verified_methods.TryGetValue
> (callback, out result))
> + return result;
> +
> + if (callback.Body.HasExceptionHandlers) {
> + foreach (ExceptionHandler eh in
> callback.Body.ExceptionHandlers) {
> + /*
> + Console.WriteLine ("HandlerStart: {0}, HandlerEnd: {1},
> FilterStart: {2}, FilterEnd: {3}, TryStart: {4}, TryEnd: {5},
> CatchType: {6}, Type: {7}",
> + GetOffset (eh.HandlerStart), GetOffset
> (eh.HandlerEnd), GetOffset (eh.FilterStart), GetOffset (eh.FilterEnd),
> + GetOffset (eh.TryStart), GetOffset
> (eh.TryEnd), eh.CatchType, eh.Type);
> + */
> +
> + /*
> + * Here we could get a lot stricter, we accept the following
> code:
> + *
> + * void Method ()
> + * {
> + * // no code here
> + * try {
> + * // ... code ..
> + * [ } catch (Exception ex } ]
> + * [ // ... code ... ]
> + * } catch {
> + * // ... code ...
> + * [ } finally { ]
> + * [ // ... code ... ]
> + * }
> + * // no code here
> + * }
> + *
> + * There are a few ways to break this
> + * - any of the catch clauses may throw an exception.
> + * - finally clause may throw an exception.
> + *
> + * The stricter rules would ensure that:
> + * - all code in catch and finally clauses are also embedded
> in a simple try { <code> } catch { <no code> } block
> + *
> + */
> +
> + // We only care about catch clauses.
> + if (eh.Type != ExceptionHandlerType.Catch)
> + continue;
> +
> + // no 'Catch ... When <condition>' clause. C# doesn't support
> it, VB does
> + if (eh.FilterStart != null || eh.FilterEnd != null)
> + continue;
> +
> + // exception clause has to start at the beginning of the method
> + if (eh.TryStart == null || eh.TryStart.Offset != 0)
> + continue;
> +
> + // and span the entire method
> + if (eh.HandlerEnd == null || eh.HandlerEnd.Offset !=
> callback.Body.CodeSize - 1)
> + continue;
> +
> + // check for empty catch clause catching every single exception
> + // we don't allow 'catch (Exception)' because in IL it's valid
> to
> + // throw any object.
> + if (eh.CatchType != null && eh.CatchType.FullName !=
> "System.Object")
> + continue;
> +
> + valid_ex_handler = true;
It would be really nice to handle throws in the catch blocks. People
do that a lot so it's a fairly serious hole in the rule.
>
> + break;
> + }
> + }
> +
> + if (verified_methods == null)
> + verified_methods = new Dictionary<MethodDefinition, bool> ();
> +
> + verified_methods.Add (callback, valid_ex_handler);
> +
> + return valid_ex_handler;
> + }
> +
> + private class ILRange {
> + public Instruction First;
> + public Instruction Last;
> + public ILRange (Instruction first)
> + {
> + First = first;
> + }
> + public void DumpAll (string prefix)
> + {
> + Instruction instr = First;
> + do {
> + Console.WriteLine ("{0}{1}", prefix, instr.ToPrettyString ());
> + if (instr == Last)
> + break;
> + instr = instr.Next;
> + } while (true);
> + }
> + }
> +
> + /// <summary>
> + /// Parses the ILRange and return all delegate pointers that
> could end up on the stack as a result of executing that code.
> + /// </summary>
> + private List<MethodDefinition> GetDelegatePointers
> (MethodDefinition method, List<List<MethodDefinition>> locals,
> ILRange range)
> + {
> + List<MethodDefinition> result = null;
> + Instruction current;
> +
> + // Check if the code does any ldftn.
> + current = range.First;
> + do {
> + if (current.OpCode.Code == Code.Ldftn) {
> + if (result == null)
> + result = new List<MethodDefinition> ();
>
> + result.Add ((current.Operand as MethodReference).Resolve ());
I think the code works, but it seems fragile to populate the list with
nulls, especially when the potential breakage does not normally happen.
>
> + }
> + if (current == range.Last)
> + break;
> + current = current.Next;
> + } while (true);
> +
> + // Check if the code loads any local variables which can be
> delegates.
> + if (locals != null && range.Last.IsLoadLocal ()) {
> + int index = range.Last.GetLoadIndex ();
> + if (locals.Count > index) {
> + List<MethodDefinition> pointers = locals [index];
> + if (pointers != null && pointers.Count != 0) {
> + if (result == null)
> + result = new List<MethodDefinition> ();
> + result.AddRange (pointers);
> + }
> + }
> + }
> +
> + // If the last opcode is a field load, check if any pointers
> have been stored in that field.
> + if (field_stores != null && (range.Last.OpCode.Code ==
> Code.Ldfld || range.Last.OpCode.Code == Code.Ldsfld)) {
> + FieldDefinition field = (range.Last.Operand as
> FieldReference).Resolve ();
> + List<MethodDefinition> pointers;
> + if (field_stores.TryGetValue (field, out pointers)) {
> + if (result == null)
> + result = new List<MethodDefinition> ();
> + result.AddRange (pointers);
> + }
> +
> + }
> +
> + return result;
> + }
> +
> +
> + /// <summary>
> + /// Verifies that all methods in the list are safe to call from
> native code,
> + /// otherwise reports the correspoding result.
> + /// </summary>
> + private void VerifyMethods (List<MethodDefinition> pointers)
> + {
> + if (pointers == null)
> + return;
> +
> + foreach (MethodDefinition pointer in pointers)
> + ReportVerifiedMethod (pointer, VerifyCallbackSafety (pointer));
> + }
> +
> + /// <summary>
> + /// Reports the result from verifying the method.
> + /// </summary>
> + private void ReportVerifiedMethod (MethodDefinition pointer, bool
> safe)
> + {
> + if (!safe) {
> + if (reported_methods == null)
> + reported_methods = new List<MethodDefinition> ();
> + reported_methods.Add (pointer);
> + Runner.Report (pointer, Severity.High, Confidence.High);
> + }
> + }
> + }
> +
> + internal static class
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRuleHelper
> + {
> +
> + public static string ToPrettyString (this Instruction instr)
> + {
> + if (instr == null)
> + return "<nil>";
> +
> + return string.Format ("IL_{0} {1} {2}", instr.Offset,
> instr.OpCode.Name, instr.Operand);
> + }
> +
> + public static int GetOffset (this Instruction instr)
> + {
> + if (instr != null)
> + return instr.Offset;
> + return -1;
> + }
> +
> + /// <summary>
> + /// Return the index of the load opcode.
> + /// This could probably go into InstructionRocks.
> + /// </summary>
> + public static int GetLoadIndex (this Instruction ins)
> + {
> + switch (ins.OpCode.Code) {
> + case Code.Ldloc_0: return 0;
> + case Code.Ldloc_1: return 1;
> + case Code.Ldloc_2: return 2;
> + case Code.Ldloc_3: return 3;
> + case Code.Ldloc: // Untested for ldloc
> + case Code.Ldloc_S: return ((VariableDefinition)
> ins.Operand).Index;
> + default:
> + throw new Exception (string.Format ("Invalid opcode: {0}",
> ins.OpCode.Name));
> + }
> + }
> +
> + /// <summary>
> + /// Return the index of the store opcode.
> + /// This could probably go into InstructionRocks.
> + /// </summary>
> + public static int GetStoreIndex (this Instruction ins)
> + {
> + switch (ins.OpCode.Code) {
> + case Code.Stloc_0: return 0;
> + case Code.Stloc_1: return 1;
> + case Code.Stloc_2: return 2;
> + case Code.Stloc_3: return 3;
> + case Code.Stloc: // Untested for stloc
> + case Code.Stloc_S: return ((VariableDefinition)
> ins.Operand).Index;
> + default:
> + throw new Exception (string.Format ("Invalid opcode: {0}",
> ins.OpCode.Name));
> + }
> + }
> + }
> +
> +}
> +
> Index: Test/
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest.cs
> ===================================================================
> --- Test/
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest.cs
> (revision 0)
> +++ Test/
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest.cs
> (revision 125705)
> @@ -0,0 +1,655 @@
> +//
> +// Unit tests for
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> +//
> +// Authors:
> +// Rolf Bjarne Kvinge <RKv...@novell.com>
> +//
> +// Copyright (C) 2009 Novell, Inc (http://www.novell.com)
> +//
> +// Permission is hereby granted, free of charge, to any person
> obtaining
> +// a copy of this software and associated documentation files (the
> +// "Software"), to deal in the Software without restriction,
> including
> +// without limitation the rights to use, copy, modify, merge,
> publish,
> +// distribute, sublicense, and/or sell copies of the Software, and to
> +// permit persons to whom the Software is furnished to do so,
> subject to
> +// the following conditions:
> +//
> +// The above copyright notice and this permission notice shall be
> +// included in all copies or substantial portions of the Software.
> +//
> +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
> +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
> +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
> HOLDERS BE
> +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
> ACTION
> +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
> CONNECTION
> +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> +//
> +
> +using System;
> +using System.Reflection;
> +using System.Runtime.InteropServices;
> +using System.Text;
> +
> +using Gendarme.Framework;
> +using Gendarme.Rules.Interoperability;
> +using Mono.Cecil;
> +
> +using NUnit.Framework;
> +using Test.Rules.Helpers;
> +using Test.Rules.Fixtures;
> +
> +namespace Test.Rules.Interoperability {
> +
> + [TestFixture]
> + public class
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest :
> MethodRuleTestFixture
> <DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule>
> + {
> +
> + private
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule rule;
> + private AssemblyDefinition assembly;
> + private TypeDefinition type;
> +
> + private TestRunner runner {
> + get {
> + return new TestRunner (new
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule ());
> + }
> + }
> +
> + public delegate void CallbackDelegate ();
> +
> + [DllImport ("library.dll")]
> + static extern void PInvokeNormal ();
> +
> + [DllImport ("library.dll")]
> + static extern void PInvokeDelegate1 (CallbackDelegate d);
> +
> + [DllImport ("library.dll")]
> + static extern void PInvokeDelegate2 (object obj, CallbackDelegate
> d);
> +
> + [DllImport ("library.dll")]
> + static extern void PInvokeDelegate3 (CallbackDelegate d1, object
> obj, CallbackDelegate d3);
> +
> +#region Callback methods
> + private CallbackDelegate CallbackOK1_Field;
> + private CallbackDelegate CallbackOK1_StaticField;
> + private void CallbackOK1 ()
> + {
> + int a, b;
> + try {
> + a = 1;
> + } catch {
> + b = 2;
> + }
> + }
> +
> + private CallbackDelegate CallbackOKStatic_Field;
> + private CallbackDelegate CallbackOKStatic_StaticField;
> + private static void CallbackOKStatic ()
> + {
> + int a, b;
> + try {
> + a = 1;
> + } catch {
> + b = 2;
> + }
> + }
> +
> + private CallbackDelegate CallbackFailEmpty_Field;
> + private CallbackDelegate CallbackFailEmpty_StaticField;
> + private void CallbackFailEmpty ()
> + {
> + }
> +
> + private CallbackDelegate CallbackFailEmptyStatic_Field;
> + private CallbackDelegate CallbackFailEmptyStatic_StaticField;
> + private static void CallbackFailEmptyStatic ()
> + {
> + }
> +
> + private void CallbackFailNoCatch ()
> + {
> + try {
> + } finally {
> + }
> + }
> +
> + private void CallbackFailNoEmptyCatch ()
> + {
> + try {
> + } catch (Exception ex) {
> + }
> + }
> +
> + private void CallbackFailNotEntireMethod1 ()
> + {
> + int i = 0;
> + try {
> + } catch {
> + }
> + }
> +
> + private void CallbackFailNotEntireMethod2 ()
> + {
> + int i;
> + try {
> + } catch {
> + }
> + i = 0;
> + }
> +
> + private void CallbackFailNoEmptyCatchEntireMethod ()
> + {
> + try {
> + int a = 1;
> + try {
> + int b = 2;
> + } catch {
> + int c = 3;
> + }
> + } catch (Exception ex) {
> + int d = 4;
> + }
> + }
> + #endregion
> +
> + [TestFixtureSetUp]
> + public void FixtureSetUp ()
> + {
> + string unit = Assembly.GetExecutingAssembly ().Location;
> + assembly = AssemblyFactory.GetAssembly (unit);
> + type = assembly.MainModule.Types
> ["Test
> .Rules
> .Interoperability
> .DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest"];
> + }
> +
> + private MethodDefinition GetTest (string name)
> + {
> + foreach (MethodDefinition method in type.Methods) {
> + if (method.Name == name)
> + return method;
> + }
> + return null;
> + }
> +
> + private TypeDefinition GetTypeTest (string name)
> + {
> + Console.WriteLine ("GetTypeTest ('{1}') NestedType count: {0}",
> this.type.NestedTypes.Count, name);
> + foreach (TypeDefinition type in this.type.NestedTypes) {
> + Console.WriteLine (" Name: '{0}'", type.Name);
> + if (type.Name == name)
> + return type;
> + }
> + return null;
> + }
> +
> + private void TestMethod_OK1 ()
> + {
> + CallbackDelegate c = CallbackFailEmpty;
> + // Create a bad delegate but not pass it to a pinvoke.
> + PInvokeNormal ();
> + }
> +
> + private void TestMethod_AnonymousOK1 ()
> + {
> + CallbackDelegate c = delegate () {};
> + // Create a bad delegate but not pass it to a pinvoke.
> + PInvokeNormal ();
> + }
> +
> + private void TestMethod_OK2 ()
> + {
> + CallbackDelegate a = CallbackFailEmpty;
> + CallbackDelegate b = CallbackOK1;
> + // Create a bad delegate and a good delegate, pass the good
> delegate.
> + PInvokeDelegate1 (b);
> + }
> +
> + private void TestMethod_AnonymousOK2 ()
> + {
> + CallbackDelegate a = delegate () { };
> + CallbackDelegate b = delegate () { try {} catch {} };
> + // Create a bad delegate and a good delegate, pass the good
> delegate.
> + PInvokeDelegate1 (b);
> + }
> +
> + private void TestMethod_CallbackOK1 ()
> + {
> + CallbackDelegate c = CallbackOK1;
> +
> + CallbackOK1_Field = CallbackOK1;
> + CallbackOK1_StaticField = CallbackOK1;
> +
> + PInvokeDelegate1 (CallbackOK1);
> + PInvokeDelegate1 (new CallbackDelegate (CallbackOK1));
> + PInvokeDelegate1 (c);
> + PInvokeDelegate1 (CallbackOK1_Field);
> + PInvokeDelegate1 (CallbackOK1_StaticField);
> + PInvokeDelegate2 (null, CallbackOK1);
> + PInvokeDelegate2 (new CallbackDelegate (CallbackOK1), null);
> + PInvokeDelegate3 (CallbackOK1_Field, null,
> CallbackOK1_StaticField);
> + }
> +
> + private void TestMethod_CallbackOKStatic ()
> + {
> + CallbackDelegate c = CallbackOKStatic;
> +
> + CallbackOKStatic_Field = CallbackOKStatic;
> + CallbackOKStatic_StaticField = CallbackOKStatic;
> + PInvokeDelegate1 (CallbackOKStatic);
> + PInvokeDelegate1 (new CallbackDelegate (CallbackOKStatic));
> + PInvokeDelegate1 (c);
> + PInvokeDelegate1 (CallbackOKStatic_Field);
> + PInvokeDelegate1 (CallbackOKStatic_StaticField);
> + PInvokeDelegate2 (null, CallbackOKStatic);
> + PInvokeDelegate2 (new CallbackDelegate (CallbackOKStatic), null);
> + PInvokeDelegate3 (CallbackOKStatic_Field, null,
> CallbackOKStatic_StaticField);
> + }
> +
> + private void TestMethod_AnonymousCallbackOK1 ()
> + {
> + // These anonymous delegates will turn out non-static since they
> access a method variable in outer scope.
> + int o = 7;
> + CallbackDelegate c = delegate () { int a, b; try { a = o; }
> catch { b = o; } };
> +
> + CallbackOKStatic_Field = delegate () { int a, b; try { a = o; }
> catch { b = o; } };
> + CallbackOKStatic_StaticField = delegate () { int a, b; try { a =
> o; } catch { b = o; } };
> + PInvokeDelegate1 (delegate () { int a, b; try { a = o; } catch
> { b = o; } });
> + PInvokeDelegate1 (new CallbackDelegate (delegate () { int a, b;
> try { a = o; } catch { b = o; } }));
> + PInvokeDelegate1 (c);
> + PInvokeDelegate1 (CallbackOKStatic_Field);
> + PInvokeDelegate1 (CallbackOKStatic_StaticField);
> + PInvokeDelegate2 (null, delegate () { int a, b; try { a = o; }
> catch { b = o; } });
> + PInvokeDelegate2 (new CallbackDelegate (delegate () { int a, b;
> try { a = o; } catch { b = o; } }), null);
> + PInvokeDelegate3 (CallbackOKStatic_Field, null,
> CallbackOKStatic_StaticField);
> + }
There should be a test where the native method argument instructions
are more than trivial loads to verify that the trace back code is
working.
>
> +
> + private void TestMethod_AnonymousCallbackOKStatic ()
> + {
> + // These anonymous delegates will turn out static given that
> they don't access any class/method variables in outer scope.
> + CallbackDelegate c = delegate () { int a, b; try { a = 1; }
> catch { b = 2; } };
> +
> + CallbackOK1_Field = delegate () { int a, b; try { a = 1; } catch
> { b = 2; } };
> + CallbackOK1_StaticField = delegate () { int a, b; try { a = 1; }
> catch { b = 2; } };
> +
> + PInvokeDelegate1 (delegate () { int a, b; try { a = 1; } catch
> { b = 2; } });
> + PInvokeDelegate1 (new CallbackDelegate (delegate () { int a, b;
> try { a = 1; } catch { b = 2; } }));
> + PInvokeDelegate1 (c);
> + PInvokeDelegate1 (CallbackOK1_Field);
> + PInvokeDelegate1 (CallbackOK1_StaticField);
> + PInvokeDelegate2 (null, delegate () { int a, b; try { a = 1; }
> catch { b = 2; } });
> + PInvokeDelegate2 (new CallbackDelegate (delegate () { int a, b;
> try { a = 1; } catch { b = 2; } }), null);
> + PInvokeDelegate3 (CallbackOK1_Field, null,
> CallbackOK1_StaticField);
> + }
> +
> + private void TestMethod_CallbackFailEmpty_a ()
> + {
> + PInvokeDelegate1 (CallbackFailEmpty);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_a ()
> + {
> + PInvokeDelegate1 (delegate () {});
> + }
> +
> + private void TestMethod_CallbackFailEmpty_b ()
> + {
> + PInvokeDelegate1 (new CallbackDelegate (CallbackFailEmpty));
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_b ()
> + {
> + PInvokeDelegate1 (new CallbackDelegate (delegate () {}));
> + }
> +
> + private void TestMethod_CallbackFailEmpty_c ()
> + {
> + CallbackDelegate c = CallbackFailEmpty;
> +
> + PInvokeDelegate1 (c);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_c ()
> + {
> + CallbackDelegate c = delegate () {};
> +
> + PInvokeDelegate1 (c);
> + }
> +
> + private void TestMethod_CallbackFailEmpty_d ()
> + {
> + CallbackFailEmptyStatic_Field = CallbackFailEmpty;
> +
> + PInvokeDelegate1 (CallbackFailEmptyStatic_Field);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_d ()
> + {
> + CallbackFailEmptyStatic_Field = delegate () {};
> +
> + PInvokeDelegate1 (CallbackFailEmptyStatic_Field);
> + }
> +
> + private void TestMethod_CallbackFailEmpty_e ()
> + {
> + CallbackFailEmptyStatic_StaticField = CallbackFailEmpty;
> +
> + PInvokeDelegate1 (CallbackFailEmptyStatic_StaticField);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_e ()
> + {
> + CallbackFailEmptyStatic_StaticField = delegate () {};
> +
> + PInvokeDelegate1 (CallbackFailEmptyStatic_StaticField);
> + }
> +
> + private void TestMethod_CallbackFailEmpty_f ()
> + {
> + PInvokeDelegate2 (null, CallbackFailEmpty);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_f ()
> + {
> + PInvokeDelegate2 (null, delegate () {});
> + }
> +
> + private void TestMethod_CallbackOKEmpty_g ()
> + {
> + PInvokeDelegate2 (new CallbackDelegate (CallbackFailEmpty), null);
> + }
> +
> + private void TestMethod_AnonymousCallbackOKEmpty_g ()
> + {
> + PInvokeDelegate2 (new CallbackDelegate (delegate () {}), null);
> + }
> +
> + private void TestMethod_CallbackFailEmpty_h ()
> + {
> + CallbackFailEmptyStatic_Field = CallbackFailEmpty;
> + CallbackFailEmptyStatic_StaticField = CallbackFailEmpty;
> +
> + PInvokeDelegate3 (CallbackFailEmptyStatic_Field, null,
> CallbackFailEmptyStatic_StaticField);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmpty_h ()
> + {
> + CallbackFailEmptyStatic_Field = delegate () {};
> + CallbackFailEmptyStatic_StaticField = delegate () {};
> +
> + PInvokeDelegate3 (CallbackFailEmptyStatic_Field, null,
> CallbackFailEmptyStatic_StaticField);
> + }
> +
> + class TestClass_InstanceFieldFail {
> + private CallbackDelegate field;
> + public void DoBadA ()
> + {
> +
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .PInvokeDelegate1 (field);
> + }
> + public void Set ()
> + { // Note that we set the field (textually) after using the
> pinvoke above.
> + field =
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .CallbackFailEmptyStatic;
> + }
> + }
> +
> + class TestClass_StaticFieldFail {
> + private static CallbackDelegate field;
> + public void Set ()
> + {
> + // Reverse textual order from instance field test above.
> + field =
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .CallbackFailEmptyStatic;
> + }
> + public void DoBadA ()
> + {
> +
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .PInvokeDelegate1 (field);
> + }
> + }
> +
> + class TestClass_AnonymousInstanceFieldFail {
> + private CallbackDelegate field;
> + public void DoBadA ()
> + {
> +
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .PInvokeDelegate1 (field);
> + }
> + public void Set ()
> + { // Note that we set the field (textually) after using the
> pinvoke above.
> + field = delegate () {};
> + }
> + }
> +
> + class TestClass_AnonymousStaticFieldFail {
> + private static CallbackDelegate field;
> + public void Set ()
> + {
> + // Reverse textual order from instance field test above.
> + field = delegate () {};
> + }
> + public void DoBadA ()
> + {
> +
> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest
> .PInvokeDelegate1 (field);
> + }
> + }
> +
> + private void TestMethod_CallbackFailEmptyStatic ()
> + {
> + PInvokeDelegate1 (CallbackFailEmptyStatic);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailEmptyStatic ()
> + {
> + PInvokeDelegate1 (delegate () { });
> + }
> +
> + private void TestMethod_CallbackFailNoCatch ()
> + {
> + PInvokeDelegate1 (CallbackFailNoCatch);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailNoCatch ()
> + {
> + PInvokeDelegate1 (delegate () { try { } finally {} });
> + }
> +
> + private void TestMethod_CallbackFailNoEmptyCatch ()
> + {
> + PInvokeDelegate1 (CallbackFailNoEmptyCatch);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailNoEmptyCatch ()
> + {
> + PInvokeDelegate1 (delegate () { try { } catch (Exception ex)
> { } });
> + }
> +
> + private void TestMethod_CallbackFailNotEntireMethod1 ()
> + {
> + PInvokeDelegate1 (CallbackFailNotEntireMethod1);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailNotEntireMethod1 ()
> + {
> + PInvokeDelegate1 (delegate () { int i = 0; try { } catch { } });
> + }
> +
> + private void TestMethod_CallbackFailNotEntireMethod2 ()
> + {
> + PInvokeDelegate1 (CallbackFailNotEntireMethod2);
> + }
> +
> + private void TestMethod_AnonymousCallbackFailNotEntireMethod2 ()
> + {
> + PInvokeDelegate1 (delegate () { int i; try { } catch { } i =
> 0; } );
> + }
> +
> + private void TestMethod_CallbackFailNoEmptyCatchEntireMethod ()
> + {
> + PInvokeDelegate1 (CallbackFailNoEmptyCatchEntireMethod);
> + }
> +
> + private void
> TestMethod_AnonymousCallbackFailNoEmptyCatchEntireMethod ()
> + {
> + PInvokeDelegate1 (delegate () { try { int a = 1; try { int b =
> 2; } catch { int c = 3; } } catch (Exception ex) { int d = 4; } });
> + }
> +
> + private void AssertTest (string name)
> + {
> + bool success = name.Contains ("OK");
> + if (success) {
> +
> AssertRuleSuccess
> <DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest> (name);
> + } else {
> +
> AssertRuleFailure
> <DelegatesPassedToNativeCodeMustIncludeExceptionHandlingTest> (name);
> + }
> + }
> +
> + private void AssertClass<T> ()
> + {
> + bool failed = false;
> + RuleResult result;
> +
> + foreach (MethodDefinition method in
> DefinitionLoader.GetTypeDefinition <T> ().Methods) {
> + result = ((TestRunner) Runner).CheckMethod (method);
> + if (result == RuleResult.Failure)
> + failed = true;
> + }
> +
> + Assert.IsTrue (failed);
> + }
> +
> + [Test]
> + public void Test_OK1 ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_OK2 ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackOK1 ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackOKStatic ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_a ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_b ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_c ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_d ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_e ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_f ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackOKEmpty_g ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmpty_h ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_StaticFieldFail ()
> + {
> + AssertClass<TestClass_StaticFieldFail> ();
> + AssertClass<TestClass_AnonymousStaticFieldFail> ();
> + }
> +
> + [Test]
> + public void Test_InstanceFieldFail ()
> + {
> + AssertClass<TestClass_InstanceFieldFail> ();
> + AssertClass<TestClass_AnonymousInstanceFieldFail> ();
> + }
> +
> + [Test]
> + public void Test_CallbackFailEmptyStatic ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailNoCatch ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailNoEmptyCatch ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailNotEntireMethod1 ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailNotEntireMethod2 ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + [Test]
> + public void Test_CallbackFailNoEmptyCatchEntireMethod ()
> + {
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_"));
> + AssertTest (MethodInfo.GetCurrentMethod ().Name.Replace
> ("Test_", "TestMethod_Anonymous"));
> + }
> +
> + }
> +}
-- Jesse
Attached is a the rule with a few changes:
* Using Log.WriteLine instead of Console.WriteLine.
* The verification for delegates has changed, the rule now verifies
that all instructions in the method are safe, i.e. the instruction
itself is either one that will never throw an exception, or covered by
a catch all exception block. This means that the following two cases
are now reported:
try {
// code
} catch {
throw new Exception (); // throwing exceptions is of course unsafe
}
try {
// code
} catch (Exception ex) {
Console.WriteLine (ex); // any method calls are unsafe
}
A lot of instructions are marked as safe (list is possibly incomplete
though), so this can still be done:
try {
return 1 / 0;
} catch {
return 123;
}
More comments inline...
On Thu, Feb 5, 2009 at 4:32 AM, Jesse Jones <jesj...@mindspring.com> wrote:
>
> I like the idea behind this rule a lot. I wonder though, if it should
> be a more general rule. For example, unhandled exceptions in thread
> callbacks are nearly as evil as unhandled exceptions in native
> delegates (see <http://msdn.microsoft.com/en-us/library/ms228965.aspx>).
>
You mean checking delegates passed to System.Thread's constructor?
This could be done, the verification logic would be exactly the same.
However I'm not sure how to name such a rule, nor where it should live
(Gendarme.Rules.Interoperability would be wrong for this).
(...)
>> + /// <code>Every delegate which is passed to native code must
>> include an exception block which spans the entire method and has a
>> catch clause with no condition.</code>
>
> This language seems a trifle clumsy. Something like "and has a catch
> all block." would be clearer I think.
Changed.
>> + public class
>> DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule : Rule,
>> IMethodRule {
>
> Should probably seal the class.
Changed.
>> + // Rule does not apply if the method has no IL
>> + if (!method.HasBody)
>> + return RuleResult.DoesNotApply;
>
> Do this first thing so allocations can be skipped. It should also be
> possible to use OpCodeBitmask to prune more methods.
Changed.
>> +
>> + // Console.WriteLine ("Checking method: {0} on type: {1}",
>> method.Name, method.DeclaringType.FullName);
>
> It's better to use the logger, especially for complex methods like
> this. The logger will allow you to dump the disassembled methods which
> is very handy and allows people to easily debug your rule without
> going in and manually un-commenting the CWLs.
>
Changed, though I think a Log.IsEnabled<T> (T obj) would be good to
add, as you can see I call Log.IsEnabled (GetType ().Name) a few times
to skip several lines of code.
>> + if (fields_loads == null)
>> + fields_loads = new List<FieldDefinition> ();
>
> I'm not so sure it's an optimization to constantly perform these null
> checks. Probably better to just allocate them up front and clear them
> on rule entry.
Changed.
>> +
>> + valid_ex_handler = true;
>
> It would be really nice to handle throws in the catch blocks. People
> do that a lot so it's a fairly serious hole in the rule.
Logic has been changed as noted in the beginning of the mail.
>>
>> + result.Add ((current.Operand as MethodReference).Resolve ());
>
> I think the code works, but it seems fragile to populate the list with
> nulls, especially when the potential breakage does not normally happen.
Changed.
Rolf
> On Thu, Feb 5, 2009 at 4:32 AM, Jesse Jones
> <jesj...@mindspring.com> wrote:
>>
>> I like the idea behind this rule a lot. I wonder though, if it should
>> be a more general rule. For example, unhandled exceptions in thread
>> callbacks are nearly as evil as unhandled exceptions in native
>> delegates (see <http://msdn.microsoft.com/en-us/library/
>> ms228965.aspx>).
>>
>
> You mean checking delegates passed to System.Thread's constructor?
> This could be done, the verification logic would be exactly the same.
Yeah, and thread pool and thread timers as well if they have the same
issue (async methods don't). I think the logic would be slightly
different though: an explicit throw from a catch block should either
be OK or be at a low confidence if the developer wants to simply throw
up his hands and allow the app to abort.
>
> However I'm not sure how to name such a rule, nor where it should live
> (Gendarme.Rules.Interoperability would be wrong for this).
I dunno, but they are pretty similar scenarios...
>> It's better to use the logger, especially for complex methods like
>> this. The logger will allow you to dump the disassembled methods
>> which
>> is very handy and allows people to easily debug your rule without
>> going in and manually un-commenting the CWLs.
>>
>
> Changed, though I think a Log.IsEnabled<T> (T obj) would be good to
> add, as you can see I call Log.IsEnabled (GetType ().Name) a few times
> to skip several lines of code.
The best way to do this is to wrap the code with `#if DEBUG`.
>>
>>> +
>>> + valid_ex_handler = true;
>>
>> It would be really nice to handle throws in the catch blocks. People
>> do that a lot so it's a fairly serious hole in the rule.
>
> Logic has been changed as noted in the beginning of the mail.
Sweet. How do the system assemblies look? Are there any false positives?
>
>
>>>
>>> + result.Add ((current.Operand
>>> as MethodReference).Resolve ());
>>
>> I think the code works, but it seems fragile to populate the list
>> with
>> nulls, especially when the potential breakage does not normally
>> happen.
>
> Changed.
I think there are similar issues with the field lists. In fact I think
there is a bug. Consider a method which uses protected fields from a
base class where the assembly for that class could not be loaded and
the following:
PInvoke(baseField1); // field doesn't resolve
baseField2 = this.AMethodWhichThrows; // field doesn't resolve
Then as far as I can tell the rule will have a false positive. The fix
is to simply not resolve the fields: the code seems like it will work
fine with FieldReferences instead.
Here are a few more notes:
1) This is just something I have found useful so feel free to ignore
it, but when figuring out what a method rule is doing it's very useful
to print the disassembled method and the offsets of instructions of
note, e.g.
Log.WriteLine (this, "\n\nChecking method: {0} on type: {1}",
method.Name, method.DeclaringType.FullName);
Log.WriteLine (this, method);
Log.WriteLine (this, " Reached a field variable store at {0:X4}, there
are {1} unsafe pointers here.", ins.Offset, pointers == null ? 0 :
pointers.Count);
This can be a bit noisy, but it does show you exactly what is going on.
2) The code no longer needs to check for null field_stores (and
probably some of the other collections).
3) The initialization of the fields_loads collection is rather
unconventional. I haven't examined it closely enough to try to see if
there are problems, but the unit tests seem to poorly test it which
scares me.
-- Jesse
On Fri, Feb 6, 2009 at 2:38 AM, Jesse Jones <jesj...@mindspring.com> wrote:
>
>> You mean checking delegates passed to System.Thread's constructor?
>> This could be done, the verification logic would be exactly the same.
>
> Yeah, and thread pool and thread timers as well if they have the same
> issue (async methods don't). I think the logic would be slightly
> different though: an explicit throw from a catch block should either
> be OK or be at a low confidence if the developer wants to simply throw
> up his hands and allow the app to abort.
>>
>> However I'm not sure how to name such a rule, nor where it should live
>> (Gendarme.Rules.Interoperability would be wrong for this).
>
> I dunno, but they are pretty similar scenarios...
I couldn't find anything on MSDN about thread timers, however the
thread pools do have this issue too.
I'll add checks for these methods at a later stage, to summarize the
rule would check delegates passed to the following methods (in
addition to pinvoke methods):
System.Threading.Thread.ctor
System.Threading.ThreadPool.QueueUserWorkItem
System.Threading.ThreadPool.RegisterWaitForSingleObject
System.Threading.ThreadPool.UnsafeQueueUserWorkItem
System.Threading.ThreadPool.UnsafeRegisterWaitForSingleObject
and I'll have to test the thread timers to see what happens there.
And I agree explicit throws in these delegates should be low confidence.
>>>
>>> It would be really nice to handle throws in the catch blocks. People
>>> do that a lot so it's a fairly serious hole in the rule.
>>
>> Logic has been changed as noted in the beginning of the mail.
>
> Sweet. How do the system assemblies look? Are there any false positives?
For all 2.0 system assemblies there are 2 failures in System.Net.dll
and 4 in System.Windows.Forms.dll, all valid.
> I think there are similar issues with the field lists. In fact I think
> there is a bug. Consider a method which uses protected fields from a
> base class where the assembly for that class could not be loaded and
> the following:
>
> PInvoke(baseField1); // field doesn't resolve
>
> baseField2 = this.AMethodWhichThrows; // field doesn't resolve
>
> Then as far as I can tell the rule will have a false positive. The fix
> is to simply not resolve the fields: the code seems like it will work
> fine with FieldReferences instead.
You're right about the issue with unresolvable field references, I've
changed it to not consider unresolvable fields at all.
Using field references instead of definitions will break if an unsafe
delegate is stored into a field reference, and the field definition is
then used when calling the pinvoke.
>
> Here are a few more notes:
>
> 1) This is just something I have found useful so feel free to ignore
> it, but when figuring out what a method rule is doing it's very useful
> to print the disassembled method and the offsets of instructions of
> note, e.g.
>
> Log.WriteLine (this, "\n\nChecking method: {0} on type: {1}",
> method.Name, method.DeclaringType.FullName);
> Log.WriteLine (this, method);
>
> Log.WriteLine (this, " Reached a field variable store at {0:X4}, there
> are {1} unsafe pointers here.", ins.Offset, pointers == null ? 0 :
> pointers.Count);
>
> This can be a bit noisy, but it does show you exactly what is going on.
Thanks, I'll have it in mind!
>
> 2) The code no longer needs to check for null field_stores (and
> probably some of the other collections).
Removed the redundant null checks I found.
> 3) The initialization of the fields_loads collection is rather
> unconventional. I haven't examined it closely enough to try to see if
> there are problems, but the unit tests seem to poorly test it which
> scares me.
I'm not quite sure what you mean here, it is initialized at class
level. (Note that I renamed it to field_loads, a more correct name).
Thanks!
Rolf
>>>> I think there are similar issues with the field lists. In fact I
>>>> think
>> there is a bug. Consider a method which uses protected fields from a
>> base class where the assembly for that class could not be loaded and
>> the following:
>>
>> PInvoke(baseField1); // field doesn't resolve
>>
>> baseField2 = this.AMethodWhichThrows; // field doesn't resolve
>>
>> Then as far as I can tell the rule will have a false positive. The
>> fix
>> is to simply not resolve the fields: the code seems like it will work
>> fine with FieldReferences instead.
>
> You're right about the issue with unresolvable field references, I've
> changed it to not consider unresolvable fields at all.
>
> Using field references instead of definitions will break if an unsafe
> delegate is stored into a field reference, and the field definition is
> then used when calling the pinvoke.
Bah, I hate that identity versus equality distinction...
>
>
>> 3) The initialization of the fields_loads collection is rather
>> unconventional. I haven't examined it closely enough to try to see if
>> there are problems, but the unit tests seem to poorly test it which
>> scares me.
>
> I'm not quite sure what you mean here, it is initialized at class
> level. (Note that I renamed it to field_loads, a more correct name).
What I mean is that you cannot simply index backwards n times to find
the instruction that loaded the nth method argument. Most rules handle
this with the Traceback instruction rock. This rule instead seems to
manually track pushes and pops, but as far as I can tell there is no
unit test to check that it does so correctly for any but the most
trivial case.
-- Jesse
Ah, you mean the list variable for the stack.
The problem with the TraceBack instruction rock is that this rule
needs to know where an expression starts and ends, and TraceBack only
says where an expression starts. Of course more tests wouldn't hurt,
not quite sure how to make it comprehensive though. One issue is that
this is sort of language/compiler dependent, gmcs doesn't generate the
same il sequences as csc, and vb/boo/etc differ even more.
Rolf
I've also added #if DEBUG instead of using Log.IsEnabled in a few
places (these are the only changes).
Rolf
I don't think you need a comprehensive test, but there should be least
one that isn't simply stepping back N instructions for the Nth argument.
-- Jesse
-- Jesse
using System;
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyVersion("0.4.84.0")]
[assembly: CLSCompliant(false)]
[assembly: ComVisible(false)]
namespace FTest
{
public static class NativeDelegateTest
{
public static void Work(Action<int> action)
{
Exception exception = null;
Callback callback = (int x) =>
{
try
{
action(x);
}
catch (Exception e)
{
exception = e;
}
};
PInvoke(callback);
if (exception != null)
Console.Error.WriteLine("{0}", exception);
}
private delegate void Callback(int x);
[DllImport("lib")]
private static extern void PInvoke(Callback callback);
}
}
-- Jesse
Index: rules/Gendarme.Rules.Interoperability/
DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
===================================================================
--- rules/Gendarme.Rules.Interoperability/
DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
(revision 126984)
+++ rules/Gendarme.Rules.Interoperability/
DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs
(working copy)
@@ -449,6 +449,9 @@
case Code.Ldloc_3: return 3;
case Code.Ldloc: // Untested for ldloc
case Code.Ldloc_S: return ((VariableDefinition) ins.Operand).Index;
+ case Code.Ldloca: // we don't care about ldloca
+ case Code.Ldloca_S: return int.MaxValue;
+
default:
throw new Exception (string.Format ("Invalid opcode: {0}",
ins.OpCode.Name));
}
Index: rules/Gendarme.Rules.Interoperability/ChangeLog
===================================================================
--- rules/Gendarme.Rules.Interoperability/ChangeLog (revision 126984)
+++ rules/Gendarme.Rules.Interoperability/ChangeLog (working copy)
@@ -1,3 +1,8 @@
+2009-01-15 Jesse Jones <jesj...@mindspring.com>
+
+ * DelegatesPassedToNativeCodeMustIncludeExceptionHandlingRule.cs:
+ Fix for ldloca instructions.
+
2008-11-30 Sebastien Pouliot <seba...@ximian.com>
* MarshalBooleansInPInvokeDeclarationsRule.cs: Use HasParameters
-- Jesse
1) Fixes the unit tests so that they run correctly with old versions
of nunit (like the one that ships with mono 2.2).
2) Includes a minor fix that prevents the rule from throwing an
exception when processing mscorlib.
-- Jesse
Please commit them.
Thanks
Sebastien
r127954
-- Jesse
So over a year after I initially started working again, I once more
lost over a day tracking down one of these exception leaking issues,
so I decided to complete it.
Attached is final, fixed patch against trunk as of today.
I've addressed the issues raised in the latest review, and there is
one extra change: the rule catches all exceptions created during its
execution, and reports a low confidence/severity failure in this case,
to address issues reported in another thread where the rule could end
up throwing exceptions with uncommon code sequences (for the record:
http://groups.google.com/group/gendarme/browse_frm/thread/c37d157ae0c9682/57f89f3abf14f2fd?tvc=1&q=Gendarme+2.6+Preview+1+is+ready+for+download#57f89f3abf14f2fd).
Rolf
On Mon, Nov 30, 2009 at 4:46 AM, Sebastien Pouliot
> --
>
> You received this message because you are subscribed to the Google Groups "Gendarme" group.
> To post to this group, send email to gend...@googlegroups.com.
> To unsubscribe from this group, send email to gendarme+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/gendarme?hl=en.
>
>
>
Sorry for the delay - I was sure I replied before vacations... but since
I can't find it in my "Sent" folder I'll assume I really needed the
vacations ;-)
On Thu, 2010-07-15 at 10:57 +0200, Rolf Bjarne Kvinge wrote:
> Hi,
>
> On Wed, Jul 14, 2010 at 4:05 AM, Sebastien Pouliot
> <sebastie...@gmail.com> wrote:
> > Hello Rolf,
> >
> > Quite a huge update :-)
> >
> > From a quick (time versus size ;-) it seems great except for two
> > things.
> >
> > 1- you removed a bunch of C.WL from the unit tests. I added (at least
> > some of) them a while ago because otherwise CSC optimize the code and
> > remove the try/catch - i.e. some unit tests fails on Windows, well
> > once compiled with CSC, using your patch (4 with CSC10*).
>
> I removed them because they're incorrect :)
>
> in a method that should pass this rule, this is bad:
>
> try {
> ...
> } catch {
> Console.WriteLine ();
> }
>
> The Console.WriteLine () statement is marked as unsafe. I removed cwls
> from catch and finally clauses because of this. I do find it strange
> that csc can optimize a method to make it unsafe - but I'll have a
> look to see what's happening.
Ok it starting to make sense (with your next emails about leave.s). I
remember some "exception-related removal optimizations" wrt csc - but
they could be related to another rule...
> >
> > 2- the "catch all" in the rule. I don't like them because they
> > (generally) hide problems (that go unreported) and lead to bad
> > results. Do you have, beside (unsupported) obfuscated code, any test
> > case that requires it ?
>
> No, I don't have any test cases that require it.
That's ok - otherwise I would have asked for a copy of the assembly ;-)
> I found it bad to
> have the rule throw exceptions, even once very rarely, because it
> would make it impossible to run gendarme on the assembly in question,
Not really, you need to ignore the rule on the target (or turn it off).
> since gendarme would just exit with a stack trace.
The console runner would (and gets me the most feedback ;-) but the
wizard would continue execution (and report the problem).
> To not hide
> problems the rule still reports a failure though.
Well a there is a few reasons (but no "god-like" command) for this:
1) It's easy to get a lot of extra (and/or ugly) code since many things
can go wrong. It can hide useful information that can be reported (or
needs more extra code). It's also easy to introduce bugs when ignoring
exceptions (see #2) which makes "aborting" a safe choice.
2) It cannot be done at the runner level (e.g. a catch {} before calling
Check* methods). This could break engines and/or (gendarme's framework
or rule-based) caches (it happened before) and make issues a lot more
complex to diagnose (e.g. adding a lot of false positive defects). All
bets are off if an exceptions occurs wrt engines or updating caches (but
it's not the case in your rule)
3) It works ;-) Seriously I don't get much feedback, even less test
cases, for false positives (but many/most complaints ;-). OTOH I get
feedback (and test cases) for unhandled exceptions since they "stop" the
analysis. Since I'm feedback-driven I need this badly!
Then there are still a few cases where "catch-all" are used by rules (or
runners). Mostly because of the (.net) framework lack of documentation
(exceptions are rarely fully documented) or bad API (e.g. validating a
Regex). I'm happy Try* methods are getting more common in .net 4) and I
intend to remove a few "catch-all" in the future.
> In any case I don't
> feel strongly either way, I don't obfuscate assemblies in the first
> place :)
I don't really care about obfuscated assemblies. OTOH some compilers are
currently under-represented in my test cases (and some do look like
obfuscated assemblies ;-)
> Have in mind that the assembly does not have to be obfuscated in the
> first place, it may happen with any normal assembly, I just haven't
> seen any vb/c# compiler generate such code sequences.
I've been building a quite varied (local) garden of assemblies. I can't
distribute most of them but I do use them for testing Gendarme before
releases. Most of them comes from the feedback I've been given ;-)
Sebastien
p.s. please commit :-)