私が書いているXslCompiledTransform.Transform
内のXsl Editor
メソッドの呼び出しから取得しているStackOverflowException
を防止または処理したいと思います。問題は、ユーザーが無限に再帰的なXsl script
を記述でき、Transform
メソッドの呼び出しで爆発することです。 (つまり、問題は典型的なプログラム上のエラーだけではありません。これは通常、このような例外の原因です。)
許可される再帰の数を検出および/または制限する方法はありますか?または、このコードが私に吹き飛ばされないようにする他のアイデアはありますか?
マイクロソフトから:
.NET Frameworkバージョン2.0以降、StackOverflowExceptionオブジェクトはtry-catchブロックでキャッチできず、対応するプロセスはデフォルトで終了します。したがって、スタックオーバーフローを検出して防止するためのコードを記述することをお勧めします。たとえば、アプリケーションが再帰に依存している場合、カウンターまたは状態条件を使用して再帰ループを終了します。
私は、コードではなく内部.NETメソッド内で例外が発生していると想定しています。
いくつかのことができます。
Processクラスを使用して、トランスフォームを別のプロセスに適用するアセンブリをロードし、メインアプリを強制終了せずに、失敗した場合に失敗をユーザーに警告できます。
編集:私はちょうどテストしました、ここにそれを行う方法があります:
メインプロセス:
// 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プロセス:
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 [〜#〜]@WilliamJockuschによる賞金の質問と元の質問は異なります。
この回答は、サードパーティライブラリの一般的な場合のStackOverflowと、それらを使用して実行できることと実行できないことに関するものです。 XslTransformの特殊なケースについては、受け入れられている回答を参照してください。
スタック上のデータが特定の制限(バイト単位)を超えるため、スタックオーバーフローが発生します。この検出がどのように機能するかの詳細は here で見つけることができます。
StackOverflowExceptionsを追跡する一般的な方法があるかどうか疑問に思っています。言い換えれば、コードのどこかに無限再帰があると仮定しますが、どこにあるのかわかりません。私はそれが起こるのを見るまで、あちこちでコードをステップ実行するよりも簡単な何らかの方法でそれを追跡したい。私はそれがどれほどハッキングされているかは気にしません。
リンクで述べたように、静的コード分析からスタックオーバーフローを検出するには、ndecidableである停止問題を解決する必要があります。 特効薬はありませんを確立したので、問題の追跡に役立つと思われるいくつかのトリックを紹介できます。
この質問はさまざまな方法で解釈できると思いますが、私は少し退屈しているので:-)、さまざまなバリエーションに分解します。
テスト環境でのスタックオーバーフローの検出
基本的に、ここでの問題は、(制限された)テスト環境があり、(拡張された)実稼働環境でスタックオーバーフローを検出することです。
SO自体を検出する代わりに、スタックの深さを設定できるという事実を活用することでこれを解決します。デバッガは必要な情報をすべて提供します。ほとんどの言語ではスタックを指定できます。サイズまたは最大再帰深度。
基本的に、スタックの深さを可能な限り小さくすることで、SOを強制しようとします。オーバーフローしない場合は、生産のために常に大きくすることができます(この場合はより安全です)スタックオーバーフローが発生した瞬間に、「有効な」スタックかどうかを手動で決定できます。
これを行うには、スタックサイズ(この場合は小さい値)をThreadパラメーターに渡し、何が起こるかを確認します。 .NETのデフォルトのスタックサイズは1 MBです。これよりも小さな値を使用します。
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();
}
}
注:このコードも以下で使用します。
一度オーバーフローすると、意味のあるSOが得られるまで、より大きな値に設定できます。
SOの前に例外を作成する
StackOverflowException
はキャッチできません。これは、発生したときにできることはあまりないことを意味します。そのため、コードに何らかの問題があると思われる場合は、場合によっては独自の例外を作成できます。これに必要なのは、現在のスタックの深さだけです。カウンターは不要です。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();
}
}
このアプローチは、コールバックメカニズムを使用するサードパーティコンポーネントを扱う場合にも機能することに注意してください。唯一必要なことは、スタックトレースでsome呼び出しをインターセプトできることです。
別のスレッドでの検出
あなたはこれを明示的に提案したので、ここでこれを説明します。
別のスレッドでSO)を検出してみてください。しかし、それはおそらく役に立たないでしょう。スタックオーバーフローが発生する可能性がありますfastこれは、このメカニズムがまったく信頼できないことを意味します...実際に使用することはお勧めしません。ただし、コードは次のとおりです:-)
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();
}
}
}
}
}
デバッガーでこれを実行し、何が起こるか楽しみましょう。
スタックオーバーフローの特性を使用する
あなたの質問の別の解釈は、「スタックオーバーフロー例外を引き起こす可能性のあるコードはどこにありますか?」です。明らかにこれに対する答えは、すべてのコードが再帰的であることです。その後、コードの各部分について、手動分析を行うことができます。
静的コード分析を使用してこれを決定することもできます。そのために必要なのは、すべてのメソッドを逆コンパイルし、無限再帰が含まれているかどうかを把握することです。これを行うコードを次に示します。
// 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>());
}
}
さて、メソッドサイクルに再帰が含まれているという事実は、スタックオーバーフローが発生することを保証するものではありません。これは、スタックオーバーフロー例外の最も可能性の高い前提条件です。要するに、これは、このコードがスタックオーバーフローcanが発生するコードの一部を決定することを意味します。これにより、ほとんどのコードが大幅に絞り込まれます。
まだ他のアプローチ
ここで説明していない他の方法も試してみてください。
XmlWriterオブジェクトの周りにラッパーを作成することをお勧めします。そのため、WriteStartElement/WriteEndElementの呼び出し量をカウントします。タグの量をある数(fe 100)に制限すると、異なる例外をスローできるようになります。 InvalidOperation。
大部分のケースで問題を解決するはずです
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));
}
}
この回答は@WilliamJockusch用です。
StackOverflowExceptionsを追跡する一般的な方法があるかどうか疑問に思っています。言い換えれば、コードのどこかに無限再帰があると仮定しますが、どこにあるのかわかりません。私はそれが起こるのを見るまで、あちこちでコードをステップ実行するよりも簡単な何らかの方法でそれを追跡したい。私はそれがどれほどハッキングされているかは気にしません。たとえば、スタックの深さをポーリングし、「高すぎる」と考えられるレベルに達した場合に文句を言う、おそらく別のスレッドからでもアクティブにできるモジュールがあると素晴らしいでしょう。たとえば、「高すぎる」を600フレームに設定し、スタックが深すぎる場合は問題にならないと考えます。そのようなものが可能です。もう1つの例は、コード内の1000回ごとのメソッド呼び出しをデバッグ出力に記録することです。これにより、オーバーローの証拠が得られる可能性は非常に高く、出力がそれほどひどく吹き飛ばされることはないでしょう。重要なのは、オーバーフローが発生している場所にチェックを書き込むことはできないということです。全体の問題は、それがどこにあるのかわからないということだからです。解決策は、私の開発環境がどのように見えるかに依存すべきではありません。つまり、特定のツールセット(VSなど)を介してC#を使用していると想定すべきではありません。
このStackOverflowをキャッチするためのデバッグテクニックを聞きたいと思っているようですので、試してみてください。
Pro's:メモリダンプは、スタックオーバーフローの原因を解決する確実な方法です。 C#MVPと私は一緒にSOのトラブルシューティングを行い、彼はそれについてブログに進みました here 。
この方法は、問題を追跡する最速の方法です。
この方法では、ログに表示される手順に従って問題を再現する必要はありません。
Con's:メモリダンプは非常に大きく、AdPlus/procdumpをプロセスにアタッチする必要があります。
Pro's:これはおそらく、メソッドのすべてのメソッドにコードを記述せずに、任意のメソッドから呼び出しスタックのサイズをチェックするコードを実装する最も簡単な方法です応用。呼び出しの前後にインターセプトできる AOP Frameworks がたくさんあります。
スタックオーバーフローの原因となっているメソッドを説明します。
アプリケーションのすべてのメソッドの入り口と出口でStackTrace().FrameCount
を確認できます。
Con's:これはパフォーマンスに影響を与えます-フックはすべてのメソッドのILに埋め込まれ、実際に「非アクティブ化」することはできません。
開発環境のツールセットによって多少異なります。
一週間前、私は問題を再現するのが難しいいくつかを追い詰めようとしていました。このQAを投稿しました ユーザーアクティビティロギング、テレメトリー(およびグローバル例外ハンドラーの変数) 私が思いついた結論は、未処理の例外が発生したときにデバッガーで問題を再現する方法を確認するための非常に単純なユーザーアクションロガーでした。
Pro's:自由にオンまたはオフにできます(つまり、イベントにサブスクライブします)。
ユーザーアクションの追跡には、すべてのメソッドをインターセプトする必要はありません。
メソッドがサブスクライブされるイベントの数もカウントできます。AOPよりもはるかに簡単です。
ログファイルは比較的小さく、問題を再現するために実行する必要があるアクションに焦点を合わせています。
ユーザーがアプリケーションをどのように使用しているかを理解するのに役立ちます。
Con's:Windowsサービスには適していません、Webアプリにはこのような優れたツールがあるはずです。
必ずしも必要ではありませんスタックオーバーフローを引き起こすメソッドを教えてくれません。
すぐに問題を取得してデバッグできるメモリダンプではなく、手動で問題を再現するログをステップスルーする必要があります。
上記のすべてのテクニックと@atlasteが投稿したいくつかのテクニックを試して、PROD環境などで実行するのが最も簡単/速い/汚い/最も受け入れやすいものを教えてください。
とにかく、このSOを追跡してください。
アプリケーションが(Xslスクリプト内の)サードパーティコードに依存している場合、まずそれらのバグから防御するかどうかを決定する必要があります。本当に防御したい場合は、個別のAppDomainで外部エラーが発生しやすいロジックを実行する必要があると思います。 StackOverflowExceptionをキャッチするのは適切ではありません。
これも確認してください question 。
今日私はstackoverflowがありました、そして、私はあなたの投稿のいくつかを読んで、Garbage Collecterを手伝うことに決めました。
私はこのような無限ループに近いものを使用していました。
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
}
}
}
代わりに、次のようにリソースをスコープ外で実行します。
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
}
それは私のために働いた、それが誰かを助けることを願っています。
.NET 4.0では、System.Runtime.ExceptionServicesのHandleProcessCorruptedStateExceptions
属性をtry/catchブロックを含むメソッドに追加できます。これは本当にうまくいきました!推奨されないかもしれませんが、動作します。
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");
}
}
}
@WilliamJockusch、私があなたの懸念を正しく理解していれば、(数学的な観点から)不可能ですalwaysハルティング問題 を解きます。それを解決するには、 超再帰アルゴリズム (たとえば 試行錯誤の述語 など)または hypercompute が可能なマシンが必要です。 (例は 次のセクション -プレビューとして利用可能- この本 で説明されています)。
実用的な観点から、あなたは知る必要があります:
現在のマシンでは、このデータはマルチタスクのために非常に変更可能であり、タスクを実行するソフトウェアについて聞いたことがありません。
不明な点がある場合はお知らせください。
見たところ、別のプロセスを開始する以外に、StackOverflowException
を処理する方法はないようです。誰かが尋ねる前に、AppDomain
を使用してみましたが、うまくいきませんでした。
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);
}
}
}
}
}
ただし、最終的に別プロセスのソリューションを使用することになった場合は、Process.Exited
およびProcess.StandardOutput
およびエラーを自分で処理して、ユーザーのエクスペリエンスを向上させます。