¿Cómo evito y / o manejo una StackOverflowException?

Me gustaría prevenir o manejar una StackOverflowException que obtengo de una llamada al método XslCompiledTransform.Transform dentro de un editor de Xsl que estoy escribiendo. El problema parece ser que el usuario puede escribir un script Xsl que es infinitamente recursivo, y simplemente explota en la llamada al método Transform. (Es decir, el problema no es solo el típico error programático, que suele ser la causa de dicha excepción).

¿Hay alguna forma de detectar y / o limitar cuántas recursiones se permiten? ¿O alguna otra idea para evitar que este código explote sobre mí?

De Microsoft:

Comenzando con .NET Framework versión 2.0, un objeto de prueba-captura no puede capturar un objeto StackOverflowException y el proceso correspondiente finaliza de forma predeterminada. En consecuencia, se recomienda a los usuarios que escriban su código para detectar y evitar un desbordamiento de la stack. Por ejemplo, si su aplicación depende de la recursión, use un contador o una condición de estado para terminar el ciclo recursivo.

Supongo que la excepción está sucediendo dentro de un método .NET interno, y no en su código.

Puedes hacer un par de cosas.

  • Escriba el código que verifica el xsl para la recursión infinita y notifica al usuario antes de aplicar una transformación (Ugh).
  • Cargue el código XslTransform en un proceso separado (Hacky, pero menos trabajo).

Puede usar la clase Process para cargar el ensamblado que aplicará la transformación en un proceso separado, y alertar al usuario de la falla si se muere, sin matar a su aplicación principal.

EDITAR: Acabo de probar, aquí está cómo hacerlo:

Proceso principal:

 // 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"); 

Proceso ApplyTransform:

 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); } } } 

NOTA La pregunta en el bounty por @WilliamJockusch y la pregunta original son diferentes.

Esta respuesta se trata de StackOverflow en el caso general de las bibliotecas de terceros y de lo que puede / no puede hacer con ellas. Si está buscando información sobre el caso especial con XslTransform, consulte la respuesta aceptada.


Los desbordamientos de stack ocurren porque los datos en la stack exceden cierto límite (en bytes). Los detalles de cómo funciona esta detección se pueden encontrar aquí .

Me pregunto si hay una forma general de rastrear StackOverflowExceptions. En otras palabras, supongamos que tengo una recursión infinita en algún lugar de mi código, pero no tengo idea de dónde. Quiero rastrearlo de alguna manera, es más fácil que recorrer el código por todos lados hasta que vea que sucede. No me importa qué tan hackish es.

Como mencioné en el enlace, detectar un desbordamiento de stack del análisis de código estático requeriría resolver el problema de detención que es indecidible . Ahora que hemos establecido que no hay una solución mágica , puedo mostrarte algunos trucos que creo que ayudan a rastrear el problema.

Creo que esta pregunta se puede interpretar de diferentes maneras, y como estoy un poco aburrido :-), la dividiré en diferentes variaciones.

Detectando un desbordamiento de stack en un entorno de prueba

Básicamente, el problema aquí es que tiene un entorno de prueba (limitado) y desea detectar un desbordamiento de stack en un entorno de producción (ampliado).

En lugar de detectar el SO en sí, lo resuelvo explotando el hecho de que se puede establecer la profundidad de la stack. El depurador le dará toda la información que necesita. La mayoría de los idiomas le permiten especificar el tamaño de la stack o la profundidad máxima de recursión.

Básicamente trato de forzar un SO haciendo que la profundidad de la stack sea lo más pequeña posible. Si no se desborda, siempre puedo hacerlo más grande (= en este caso: más seguro) para el entorno de producción. En el momento en que obtienes un desbordamiento de stack, puedes decidir manualmente si es “válido” o no.

Para hacer esto, pase el tamaño de la stack (en nuestro caso: un valor pequeño) a un parámetro Thread, y vea qué sucede. El tamaño de stack predeterminado en .NET es de 1 MB, vamos a utilizar un valor mucho menor:

 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(); } } 

Nota: vamos a usar este código a continuación también.

Una vez que se desborda, puede establecerlo en un valor mayor hasta que obtenga un SO que tenga sentido.

Creando excepciones antes que TI

StackOverflowException no es capturable. Esto significa que no hay mucho que puedas hacer cuando haya sucedido. Entonces, si cree que algo va a salir mal en su código, puede hacer su propia excepción en algunos casos. Lo único que necesita para esto es la profundidad de stack actual; no hay necesidad de un contador, puede usar los valores reales de .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(); } } 

Tenga en cuenta que este enfoque también funciona si se trata de componentes de terceros que utilizan un mecanismo de callback. Lo único que se requiere es interceptar algunas llamadas en el seguimiento de la stack.

Detección en un hilo separado

Usted sugirió esto explícitamente, así que aquí va este.

Puedes intentar detectar un SO en un hilo separado … pero probablemente no te sirva de nada. Un desbordamiento de stack puede suceder rápidamente , incluso antes de obtener un cambio de contexto. Esto significa que este mecanismo no es confiable en absoluto … No recomendaría realmente usarlo . Sin embargo, fue divertido de construir, así que aquí está el código 🙂

 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(); } } } } } 

Ejecuta esto en el depurador y diviértete con lo que sucede.

Usando las características de un desbordamiento de stack

Otra interpretación de su pregunta es: “¿Dónde están las piezas de código que podrían causar una excepción de desbordamiento de stack?”. Obviamente, la respuesta es: todo el código con recursividad. Para cada fragmento de código, puede hacer un análisis manual.

También es posible determinar esto usando el análisis de código estático. Lo que debe hacer para eso es descomstackr todos los métodos y averiguar si contienen una recursión infinita. Aquí hay un código que lo hace por usted:

 // 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 result = new HashSet(); 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 calling = new Dictionary(); 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()) { 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()); } } 

Ahora bien, el hecho de que un ciclo de método contenga recursión no significa en modo alguno que se produzca un desbordamiento de la stack; es simplemente la condición previa más probable para la excepción de desbordamiento de la stack. En resumen, esto significa que este código determinará las piezas de código donde puede ocurrir un desbordamiento de la stack, lo que debería limitar considerablemente la mayoría del código.

Sin embargo, otros enfoques

Hay otros enfoques que puedes probar que no he descrito aquí.

  1. Manejando el desbordamiento de stack al hospedar el proceso CLR y manejarlo. Tenga en cuenta que todavía no puede ‘atraparlo’.
  2. Cambiando todo el código IL, construyendo otra DLL, agregando verificaciones en la recursión. Sí, eso es bastante posible (lo he implementado en el pasado :-); es difícil e involucra una gran cantidad de código para hacerlo bien.
  3. Use la API de perfiles .NET para capturar todas las llamadas a métodos y usarlas para descubrir desbordamientos de stack. Por ejemplo, puede implementar comprobaciones de que si encuentra el mismo método X veces en su árbol de llamadas, usted da una señal. Hay un proyecto aquí que te dará una ventaja.

Sugeriría crear un contenedor alrededor del objeto XmlWriter, por lo que contaría la cantidad de llamadas a WriteStartElement / WriteEndElement, y si limita la cantidad de tags a algún número (fe 100), podría lanzar una excepción diferente, por ejemplo – Operación inválida.

Eso debería resolver el problema en la mayoría de los casos

 public class LimitedDepthXmlWriter : XmlWriter { private readonly XmlWriter _innerWriter; private readonly int _maxDepth; private int _depth; public LimitedDepthXmlWriter(XmlWriter innerWriter): this(innerWriter, 100) { } public LimitedDepthXmlWriter(XmlWriter innerWriter, int maxDepth) { _maxDepth = maxDepth; _innerWriter = innerWriter; } public override void Close() { _innerWriter.Close(); } public override void Flush() { _innerWriter.Flush(); } public override string LookupPrefix(string ns) { return _innerWriter.LookupPrefix(ns); } public override void WriteBase64(byte[] buffer, int index, int count) { _innerWriter.WriteBase64(buffer, index, count); } public override void WriteCData(string text) { _innerWriter.WriteCData(text); } public override void WriteCharEntity(char ch) { _innerWriter.WriteCharEntity(ch); } public override void WriteChars(char[] buffer, int index, int count) { _innerWriter.WriteChars(buffer, index, count); } public override void WriteComment(string text) { _innerWriter.WriteComment(text); } public override void WriteDocType(string name, string pubid, string sysid, string subset) { _innerWriter.WriteDocType(name, pubid, sysid, subset); } public override void WriteEndAttribute() { _innerWriter.WriteEndAttribute(); } public override void WriteEndDocument() { _innerWriter.WriteEndDocument(); } public override void WriteEndElement() { _depth--; _innerWriter.WriteEndElement(); } public override void WriteEntityRef(string name) { _innerWriter.WriteEntityRef(name); } public override void WriteFullEndElement() { _innerWriter.WriteFullEndElement(); } public override void WriteProcessingInstruction(string name, string text) { _innerWriter.WriteProcessingInstruction(name, text); } public override void WriteRaw(string data) { _innerWriter.WriteRaw(data); } public override void WriteRaw(char[] buffer, int index, int count) { _innerWriter.WriteRaw(buffer, index, count); } public override void WriteStartAttribute(string prefix, string localName, string ns) { _innerWriter.WriteStartAttribute(prefix, localName, ns); } public override void WriteStartDocument(bool standalone) { _innerWriter.WriteStartDocument(standalone); } public override void WriteStartDocument() { _innerWriter.WriteStartDocument(); } public override void WriteStartElement(string prefix, string localName, string ns) { if (_depth++ > _maxDepth) ThrowException(); _innerWriter.WriteStartElement(prefix, localName, ns); } public override WriteState WriteState { get { return _innerWriter.WriteState; } } public override void WriteString(string text) { _innerWriter.WriteString(text); } public override void WriteSurrogateCharEntity(char lowChar, char highChar) { _innerWriter.WriteSurrogateCharEntity(lowChar, highChar); } public override void WriteWhitespace(string ws) { _innerWriter.WriteWhitespace(ws); } private void ThrowException() { throw new InvalidOperationException(string.Format("Result xml has more than {0} nested tags. It is possible that xslt transformation contains an endless recursive call.", _maxDepth)); } } 

Si su aplicación depende del código de una parte 3d (en scripts Xsl), entonces debe decidir primero si desea defenderse de los errores en ellos o no. Si realmente quieres defenderte, entonces creo que deberías ejecutar tu lógica propensa a errores externos en diferentes AppDomains. La captura de StackOverflowException no es buena.

Verifique también esta pregunta .

Tuve un stackoverflow hoy y leí algunas de sus publicaciones y decidí ayudar al Garbage Collecter.

Solía ​​tener un ciclo casi infinito como este:

  class Foo { public Foo() { Go(); } public void Go() { for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f) { byte[] b = new byte[1]; // Causes stackoverflow } } } 

En cambio, deje que el recurso se quede sin scope como este:

 class Foo { public Foo() { GoHelper(); } public void GoHelper() { for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f) { Go(); } } public void Go() { byte[] b = new byte[1]; // Will get cleaned by GC } // right now } 

Me funcionó, espero que ayude a alguien.

Esta respuesta es para @WilliamJockusch.

Me pregunto si hay una forma general de rastrear StackOverflowExceptions. En otras palabras, supongamos que tengo una recursión infinita en algún lugar de mi código, pero no tengo idea de dónde. Quiero rastrearlo de alguna manera, es más fácil que recorrer el código por todos lados hasta que vea que sucede. No me importa qué tan hackish es. Por ejemplo, sería genial tener un módulo que pudiera activar, quizás incluso desde otro hilo, que sondeara la profundidad de la stack y me quejara si llegaba a un nivel que consideraba “demasiado alto”. Por ejemplo, podría establecer “demasiado alto” para 600 cuadros, suponiendo que si la stack era demasiado profunda, tiene que ser un problema. Es algo como eso posible. Otro ejemplo sería registrar cada 1000 llamada de método dentro de mi código a la salida de depuración. Las posibilidades de que esto obtenga alguna evidencia del sobregolpe sería bastante bueno, y probablemente no explote demasiado la producción. La clave es que no puede implicar escribir un cheque donde sea que ocurra el desbordamiento. Porque todo el problema es que no sé dónde está eso. Preferiblemente, la solución no debe depender de cómo se vea mi entorno de desarrollo; es decir, no debe asumir que estoy usando C # a través de un conjunto de herramientas específico (por ejemplo, VS).

Parece que estás ansioso por escuchar algunas técnicas de depuración para atrapar este StackOverflow, así que pensé en compartir algunas para que lo probaras.

1. Volcados de memoria.

Pro’s : Memory Dumps son una manera segura de resolver la causa de un desbordamiento de stack. AC # MVP y yo trabajamos juntos para resolver un SO y él comenzó a bloguear aquí .

Este método es la forma más rápida de rastrear el problema.

Este método no requerirá que reproduzca problemas siguiendo los pasos que se ven en los registros.

Con : los volcados de memoria son muy grandes y debe adjuntar AdPlus / procdump al proceso.

2. Progtwigción Orientada a Aspectos.

Pro : Esta es probablemente la forma más fácil para que implemente código que verifique el tamaño de la stack de llamadas desde cualquier método sin escribir código en todos los métodos de su aplicación. Hay un montón de marcos de trabajo AOP que le permiten interceptar antes y después de las llamadas.

Te dirá los métodos que están causando el desbordamiento de la stack.

Le permite comprobar StackTrace().FrameCount a la entrada y salida de todos los métodos en su aplicación.

Con : tendrá un impacto en el rendimiento: los ganchos están incrustados en el IL para cada método y realmente no se puede “desactivar”.

De alguna manera depende de su conjunto de herramientas de entorno de desarrollo.

3. Logging User Activity.

Hace una semana estaba tratando de encontrar varios problemas difíciles de reproducir. Publiqué este registro de actividad de usuario de QA , telemetría (y variables en controladores de excepciones globales) . La conclusión a la que llegué fue un usuario-acciones-registrador muy simple para ver cómo reproducir problemas en un depurador cuando ocurre una excepción no controlada.

Pro : puede activarlo o desactivarlo a voluntad (es decir, suscribirse a eventos).

El seguimiento de las acciones del usuario no requiere la interceptación de todos los métodos.

Puede contar la cantidad de eventos que los métodos se suscriben demasiado más simplemente que con AOP .

Los archivos de registro son relativamente pequeños y se centran en las acciones que debe realizar para reproducir el problema.

Puede ayudarlo a comprender cómo los usuarios usan su aplicación.

Con : no es adecuado para un servicio de Windows y estoy seguro de que hay mejores herramientas como esta para aplicaciones web .

No necesariamente le informa los métodos que causan el desbordamiento de stack.

Requiere que recorra los registros de forma manual reproduciendo los problemas en lugar de un Volcado de memoria donde puede obtenerlos y depurarlos de inmediato.


Tal vez puedas probar todas las técnicas que mencioné arriba y algunas que @atlaste publicó y decirnos cuál encontraste que fue la más fácil / más rápida / más sucia / la más aceptable para ejecutar en un entorno PROD / etc.

De todos modos, buena suerte rastreando este SO.

Con .NET 4.0 Puede agregar el atributo HandleProcessCorruptedStateExceptions de System.Runtime.ExceptionServices al método que contiene el bloque try / catch. Esto realmente funcionó! Tal vez no recomendado, pero funciona.

 using System; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.ExceptionServices; namespace ExceptionCatching { public class Test { public void StackOverflow() { StackOverflow(); } public void CustomException() { throw new Exception(); } public unsafe void AccessViolation() { byte b = *(byte*)(8762765876); } } class Program { [HandleProcessCorruptedStateExceptions] static void Main(string[] args) { Test test = new Test(); try { //test.StackOverflow(); test.AccessViolation(); //test.CustomException(); } catch { Console.WriteLine("Caught."); } Console.WriteLine("End of program"); } } } 

Por lo que parece, aparte de iniciar otro proceso, no parece haber ninguna forma de manejar una StackOverflowException . Antes de que alguien más pregunta, traté de usar AppDomain , pero eso no funcionó:

 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading; namespace StackOverflowExceptionAppDomainTest { class Program { static void recrusiveAlgorithm() { recrusiveAlgorithm(); } static void Main(string[] args) { if(args.Length>0&&args[0]=="--child") { recrusiveAlgorithm(); } else { var domain = AppDomain.CreateDomain("Child domain to test StackOverflowException in."); domain.ExecuteAssembly(Assembly.GetEntryAssembly().CodeBase, new[] { "--child" }); domain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => { Console.WriteLine("Detected unhandled exception: " + e.ExceptionObject.ToString()); }; while (true) { Console.WriteLine("*"); Thread.Sleep(1000); } } } } } 

Sin embargo, si termina utilizando la solución de proceso separado, recomendaría usar Process.Exited y Process.StandardOutput y manejar los errores usted mismo, para brindarles a sus usuarios una mejor experiencia.

@WilliamJockusch, si entendí correctamente su preocupación, no es posible (desde un punto de vista matemático) identificar siempre una recursión infinita, ya que significaría resolver el problema de Detención . Para resolverlo necesitaría un algoritmo super recursivo (como predicados de prueba y error, por ejemplo) o una máquina que puede hipercomputar (se explica un ejemplo en la siguiente sección , disponible como vista previa, de este libro ).

Desde un punto de vista práctico, debes saber:

  • Cuánta memoria de stack le queda en el momento dado
  • Cuánta memoria de stack necesitará su método recursivo en el momento dado para la salida específica.

Tenga en cuenta que, con las máquinas actuales, estos datos son extremadamente variables debido a la multitarea y no he oído hablar de un software que realice la tarea.

Avíseme si algo no está claro.

Puede leer esta propiedad cada pocas llamadas, Environment.StackTrace , y si el stacktrace excedió un umbral específico preestablecido, puede devolver la función.

También debería intentar reemplazar algunas funciones recursivas con bucles.