I would like to either prevent or handle a StackOverflowException
that I am getting from a call to the XslCompiledTransform.Transform
method within an Xsl Editor
I am writing. The problem seems to be that the user can write an Xsl script
that is infinitely recursive, and it just blows up on the call to the Transform
method. (That is, the problem is not just the typical programmatic error, which is usually the cause of such an exception.)
Is there a way to detect and/or limit how many recursions are allowed? Or any other ideas to keep this code from just blowing up on me?
Starting with the . NET Framework 2.0, you can't catch a StackOverflowException object with a try / catch block, and the corresponding process is terminated by default. Consequently, you should write your code to detect and prevent a stack overflow.
A StackOverflowException is thrown when the execution stack overflows because it contains too many nested method calls. For example, suppose you have an app as follows: C# Copy. using System; namespace temp { class Program { static void Main(string[] args) { Main(args); // Oops, this recursion won't stop. } } }
Catching a stack overflow exception is a bad idea; you are treating the symptoms without addressing the underlying cause. In the case of the use of recursion and overflow of the set options of the JVM the SO is a normal negative answer of a correct program. We MUST catch it and react.
StackOverflowError is a runtime error which points to serious problems that cannot be caught by an application. The java. lang. StackOverflowError indicates that the application stack is exhausted and is usually caused by deep or infinite recursion.
From Microsoft:
Starting with the .NET Framework version 2.0, a StackOverflowException object cannot be caught by a try-catch block and the corresponding process is terminated by default. Consequently, users are advised to write their code to detect and prevent a stack overflow. For example, if your application depends on recursion, use a counter or a state condition to terminate the recursive loop.
I'm assuming the exception is happening within an internal .NET method, and not in your code.
You can do a couple things.
You can use the Process class to load the assembly that will apply the transform into a separate process, and alert the user of the failure if it dies, without killing your main app.
EDIT: I just tested, here is how to do it:
MainProcess:
// This is just an example, obviously you'll want to pass args to this. Process p1 = new Process(); p1.StartInfo.FileName = "ApplyTransform.exe"; p1.StartInfo.UseShellExecute = false; p1.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; p1.Start(); p1.WaitForExit(); if (p1.ExitCode == 1) Console.WriteLine("StackOverflow was thrown");
ApplyTransform Process:
class Program { static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); throw new StackOverflowException(); } // We trap this, we can't save the process, // but we can prevent the "ILLEGAL OPERATION" window static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.IsTerminating) { Environment.Exit(1); } } }
NOTE The question in the bounty by @WilliamJockusch and the original question are different.
This answer is about StackOverflow's in the general case of third-party libraries and what you can/can't do with them. If you're looking about the special case with XslTransform, see the accepted answer.
Stack overflows happen because the data on the stack exceeds a certain limit (in bytes). The details of how this detection works can be found here.
I'm wondering if there is a general way to track down StackOverflowExceptions. In other words, suppose I have infinite recursion somewhere in my code, but I have no idea where. I want to track it down by some means that is easier than stepping through code all over the place until I see it happening. I don't care how hackish it is.
As I mentioned in the link, detecting a stack overflow from static code analysis would require solving the halting problem which is undecidable. Now that we've established that there is no silver bullet, I can show you a few tricks that I think helps track down the problem.
I think this question can be interpreted in different ways, and since I'm a bit bored :-), I'll break it down into different variations.
Detecting a stack overflow in a test environment
Basically the problem here is that you have a (limited) test environment and want to detect a stack overflow in an (expanded) production environment.
Instead of detecting the SO itself, I solve this by exploiting the fact that the stack depth can be set. The debugger will give you all the information you need. Most languages allow you to specify the stack size or the max recursion depth.
Basically I try to force a SO by making the stack depth as small as possible. If it doesn't overflow, I can always make it bigger (=in this case: safer) for the production environment. The moment you get a stack overflow, you can manually decide if it's a 'valid' one or not.
To do this, pass the stack size (in our case: a small value) to a Thread parameter, and see what happens. The default stack size in .NET is 1 MB, we're going to use a way smaller value:
class StackOverflowDetector { static int Recur() { int variable = 1; return variable + Recur(); } static void Start() { int depth = 1 + Recur(); } static void Main(string[] args) { Thread t = new Thread(Start, 1); t.Start(); t.Join(); Console.WriteLine(); Console.ReadLine(); } }
Note: we're going to use this code below as well.
Once it overflows, you can set it to a bigger value until you get a SO that makes sense.
Creating exceptions before you SO
The StackOverflowException
is not catchable. This means there's not much you can do when it has happened. So, if you believe something is bound to go wrong in your code, you can make your own exception in some cases. The only thing you need for this is the current stack depth; there's no need for a counter, you can use the real values from .NET:
class StackOverflowDetector { static void CheckStackDepth() { if (new StackTrace().FrameCount > 10) // some arbitrary limit { throw new StackOverflowException("Bad thread."); } } static int Recur() { CheckStackDepth(); int variable = 1; return variable + Recur(); } static void Main(string[] args) { try { int depth = 1 + Recur(); } catch (ThreadAbortException e) { Console.WriteLine("We've been a {0}", e.ExceptionState); } Console.WriteLine(); Console.ReadLine(); } }
Note that this approach also works if you are dealing with third-party components that use a callback mechanism. The only thing required is that you can intercept some calls in the stack trace.
Detection in a separate thread
You explicitly suggested this, so here goes this one.
You can try detecting a SO in a separate thread.. but it probably won't do you any good. A stack overflow can happen fast, even before you get a context switch. This means that this mechanism isn't reliable at all... I wouldn't recommend actually using it. It was fun to build though, so here's the code :-)
class StackOverflowDetector { static int Recur() { Thread.Sleep(1); // simulate that we're actually doing something :-) int variable = 1; return variable + Recur(); } static void Start() { try { int depth = 1 + Recur(); } catch (ThreadAbortException e) { Console.WriteLine("We've been a {0}", e.ExceptionState); } } static void Main(string[] args) { // Prepare the execution thread Thread t = new Thread(Start); t.Priority = ThreadPriority.Lowest; // Create the watch thread Thread watcher = new Thread(Watcher); watcher.Priority = ThreadPriority.Highest; watcher.Start(t); // Start the execution thread t.Start(); t.Join(); watcher.Abort(); Console.WriteLine(); Console.ReadLine(); } private static void Watcher(object o) { Thread towatch = (Thread)o; while (true) { if (towatch.ThreadState == System.Threading.ThreadState.Running) { towatch.Suspend(); var frames = new System.Diagnostics.StackTrace(towatch, false); if (frames.FrameCount > 20) { towatch.Resume(); towatch.Abort("Bad bad thread!"); } else { towatch.Resume(); } } } } }
Run this in the debugger and have fun of what happens.
Using the characteristics of a stack overflow
Another interpretation of your question is: "Where are the pieces of code that could potentially cause a stack overflow exception?". Obviously the answer of this is: all code with recursion. For each piece of code, you can then do some manual analysis.
It's also possible to determine this using static code analysis. What you need to do for that is to decompile all methods and figure out if they contain an infinite recursion. Here's some code that does that for you:
// A simple decompiler that extracts all method tokens (that is: call, callvirt, newobj in IL) internal class Decompiler { private Decompiler() { } static Decompiler() { singleByteOpcodes = new OpCode[0x100]; multiByteOpcodes = new OpCode[0x100]; FieldInfo[] infoArray1 = typeof(OpCodes).GetFields(); for (int num1 = 0; num1 < infoArray1.Length; num1++) { FieldInfo info1 = infoArray1[num1]; if (info1.FieldType == typeof(OpCode)) { OpCode code1 = (OpCode)info1.GetValue(null); ushort num2 = (ushort)code1.Value; if (num2 < 0x100) { singleByteOpcodes[(int)num2] = code1; } else { if ((num2 & 0xff00) != 0xfe00) { throw new Exception("Invalid opcode: " + num2.ToString()); } multiByteOpcodes[num2 & 0xff] = code1; } } } } private static OpCode[] singleByteOpcodes; private static OpCode[] multiByteOpcodes; public static MethodBase[] Decompile(MethodBase mi, byte[] ildata) { HashSet<MethodBase> result = new HashSet<MethodBase>(); Module module = mi.Module; int position = 0; while (position < ildata.Length) { OpCode code = OpCodes.Nop; ushort b = ildata[position++]; if (b != 0xfe) { code = singleByteOpcodes[b]; } else { b = ildata[position++]; code = multiByteOpcodes[b]; b |= (ushort)(0xfe00); } switch (code.OperandType) { case OperandType.InlineNone: break; case OperandType.ShortInlineBrTarget: case OperandType.ShortInlineI: case OperandType.ShortInlineVar: position += 1; break; case OperandType.InlineVar: position += 2; break; case OperandType.InlineBrTarget: case OperandType.InlineField: case OperandType.InlineI: case OperandType.InlineSig: case OperandType.InlineString: case OperandType.InlineTok: case OperandType.InlineType: case OperandType.ShortInlineR: position += 4; break; case OperandType.InlineR: case OperandType.InlineI8: position += 8; break; case OperandType.InlineSwitch: int count = BitConverter.ToInt32(ildata, position); position += count * 4 + 4; break; case OperandType.InlineMethod: int methodId = BitConverter.ToInt32(ildata, position); position += 4; try { if (mi is ConstructorInfo) { result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes)); } else { result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments())); } } catch { } break; default: throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType); } } return result.ToArray(); } } class StackOverflowDetector { // This method will be found: static int Recur() { CheckStackDepth(); int variable = 1; return variable + Recur(); } static void Main(string[] args) { RecursionDetector(); Console.WriteLine(); Console.ReadLine(); } static void RecursionDetector() { // First decompile all methods in the assembly: Dictionary<MethodBase, MethodBase[]> calling = new Dictionary<MethodBase, MethodBase[]>(); var assembly = typeof(StackOverflowDetector).Assembly; foreach (var type in assembly.GetTypes()) { foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).OfType<MethodBase>()) { var body = member.GetMethodBody(); if (body!=null) { var bytes = body.GetILAsByteArray(); if (bytes != null) { // Store all the calls of this method: var calls = Decompiler.Decompile(member, bytes); calling[member] = calls; } } } } // Check every method: foreach (var method in calling.Keys) { // If method A -> ... -> method A, we have a possible infinite recursion CheckRecursion(method, calling, new HashSet<MethodBase>()); } }
Now, the fact that a method cycle contains recursion, is by no means a guarantee that a stack overflow will happen - it's just the most likely precondition for your stack overflow exception. In short, this means that this code will determine the pieces of code where a stack overflow can occur, which should narrow down most code considerably.
Yet other approaches
There are some other approaches you can try that I haven't described here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With