-
Couldn't load subscription status.
- Fork 35
Fix native stack frame parsing crash for frames without symbols #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ jobs: | |
| projectPath: | ||
| - test-package | ||
| unityVersion: | ||
| - 2019.4.40f1 | ||
| - 2022.3.56f1 | ||
| targetPlatform: | ||
| - StandaloneOSX | ||
| - StandaloneWindows | ||
|
|
@@ -48,5 +48,6 @@ jobs: | |
| unityVersion: ${{ matrix.unityVersion }} | ||
| targetPlatform: ${{ matrix.targetPlatform }} | ||
| projectPath: ${{ matrix.projectPath }}/ | ||
| allowDirtyBuild: true | ||
| allowDirtyBuild: true | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. broken yaml There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup |
||
| testMode: editmode | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,13 +136,13 @@ private List<BacktraceStackFrame> ConvertStackFrames(IEnumerable<string> frames) | |
|
|
||
| } | ||
|
|
||
| //methodname index should be greater than 0 AND '(' should be before ')' | ||
| if (methodNameEndIndex < 1 && frameString[methodNameEndIndex - 1] != '(') | ||
| // we require a '(' that appears before this ')' | ||
| int openParenIndex = frameString.LastIndexOf('(', methodNameEndIndex); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo paren |
||
| if (openParenIndex == -1 || openParenIndex > methodNameEndIndex) | ||
| { | ||
| result.Add(new BacktraceStackFrame() | ||
| { | ||
| FunctionName = frame | ||
| }); | ||
| // invalid shape: no matching '(' before ')' | ||
| result.Add(new BacktraceStackFrame { FunctionName = frame }); | ||
| continue; | ||
| } | ||
|
|
||
| result.Add(ConvertFrame(frameString, methodNameEndIndex)); | ||
|
|
@@ -271,63 +271,98 @@ private BacktraceStackFrame SetJITStackTraceInformation(string frameString) | |
| } | ||
|
|
||
| /// <summary> | ||
| /// Try to convert native stack frame | ||
| /// Try to safely convert a native stack frame string into a Backtrace stack frame. | ||
| /// Handles frames with or without symbols (e.g. "0xADDR (Module)" or "0xADDR (Module) Symbol"). | ||
| /// Prevents ArgumentOutOfRangeException by validating index ranges and allowing empty symbols. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whats the point of adding this info to the summary? |
||
| /// </summary> | ||
| /// <param name="frameString">Native stack frame</param> | ||
| /// <returns>Backtrace stack frame</returns> | ||
| /// <param name="frameString">Raw native stack frame line to parse.</param> | ||
| /// <returns>Parsed Backtrace stack frame containing address, library, function name, and optional line number.</returns> | ||
| private BacktraceStackFrame SetNativeStackTraceInformation(string frameString) | ||
| { | ||
| var stackFrame = new BacktraceStackFrame | ||
| { | ||
| StackFrameType = Types.BacktraceStackFrameType.Native | ||
| StackFrameType = Types.BacktraceStackFrameType.Native, | ||
| FunctionName = string.Empty, | ||
| Line = 0 | ||
|
Comment on lines
+285
to
+286
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure you need to set them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, needed to guarantee consistent defaults for native frames (especially when the parser exits early). It avoids nulls and keeps the frame type marked as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah but why we should avoid nulls? I would say null in this case gives us important information - do not set entry in the json field. Are you sure symbolication process later will handle it properly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solid point, exactly what I needed! |
||
| }; | ||
| // parse address | ||
| var addressSubstringIndex = frameString.IndexOf(' '); | ||
| if (addressSubstringIndex == -1) | ||
|
|
||
| if (string.IsNullOrEmpty(frameString)) | ||
| { | ||
| stackFrame.FunctionName = frameString; | ||
| return stackFrame; | ||
| } | ||
| stackFrame.Address = frameString.Substring(0, addressSubstringIndex); | ||
| var indexPointer = addressSubstringIndex + 1; | ||
|
|
||
| // parse library | ||
| if (frameString[indexPointer] == '(') | ||
| frameString = frameString.Trim(); | ||
|
|
||
| // Address: starts with "0x" and ends at first space | ||
| int index = 0; | ||
| if (frameString.StartsWith("0x", StringComparison.Ordinal)) | ||
| { | ||
| indexPointer = indexPointer + 1; | ||
| var libraryNameSubstringIndex = frameString.IndexOf(')', indexPointer); | ||
| stackFrame.Library = frameString.Substring(indexPointer, libraryNameSubstringIndex - indexPointer); | ||
| indexPointer = libraryNameSubstringIndex + 2; | ||
| int space = frameString.IndexOf(' '); | ||
| if (space > 2) | ||
| { | ||
| stackFrame.Address = frameString.Substring(0, space); | ||
| index = space + 1; | ||
| } | ||
| else | ||
| { | ||
| // if Unknown format keep raw text and return | ||
| stackFrame.FunctionName = frameString; | ||
| return stackFrame; | ||
| } | ||
| } | ||
|
|
||
| // Library: Module | ||
| if (index < frameString.Length && frameString[index] == '(') | ||
| { | ||
| index++; | ||
| int close = frameString.IndexOf(')', index); | ||
| // if ')' missing leave Library null and continue | ||
| if (close > -1) | ||
| { | ||
| stackFrame.Library = frameString.Substring(index, close - index); | ||
| index = close + 1; | ||
| if (index < frameString.Length && frameString[index] == ' ') | ||
| { | ||
| index++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| stackFrame.FunctionName = frameString.Substring(indexPointer); | ||
| //cleanup function name | ||
| if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)")) | ||
| // 3) symbol | ||
| stackFrame.FunctionName = (index < frameString.Length) | ||
| ? frameString.Substring(index).Trim() | ||
| : string.Empty; | ||
|
|
||
| // 4) Normalize known wrappers | ||
| if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)", StringComparison.Ordinal)) | ||
| { | ||
| stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper managed-to-native)", string.Empty).Trim(); | ||
| } | ||
|
|
||
| if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)")) | ||
| if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)", StringComparison.Ordinal)) | ||
| { | ||
| stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper runtime-invoke)", string.Empty).Trim(); | ||
| } | ||
|
|
||
| // try to find source code information | ||
| int sourceCodeStartIndex = stackFrame.FunctionName.IndexOf('['); | ||
| int sourceCodeEndIndex = stackFrame.FunctionName.IndexOf(']'); | ||
| if (sourceCodeStartIndex != -1 && sourceCodeEndIndex != -1) | ||
| // [file:line] suffix source code information | ||
| int srcStart = stackFrame.FunctionName.IndexOf('['); | ||
| int srcEnd = stackFrame.FunctionName.IndexOf(']'); | ||
| if (srcStart != -1 && srcEnd != -1 && srcEnd > srcStart) | ||
| { | ||
| sourceCodeStartIndex = sourceCodeStartIndex + 1; | ||
| var sourceCodeInformation = stackFrame.FunctionName.Substring( | ||
| sourceCodeStartIndex, | ||
| sourceCodeEndIndex - sourceCodeStartIndex); | ||
|
|
||
| var sourceCodeParts = sourceCodeInformation.Split(new char[] { ':' }, 2); | ||
| if (sourceCodeParts.Length == 2) | ||
| srcStart++; | ||
| var src = stackFrame.FunctionName.Substring(srcStart, srcEnd - srcStart); | ||
| var parts = src.Split(new char[] { ':' }, 2); | ||
| if (parts.Length == 2 && int.TryParse(parts[1], out var line)) | ||
| { | ||
| int.TryParse(sourceCodeParts[1], out stackFrame.Line); | ||
| stackFrame.Library = sourceCodeParts[0]; | ||
| stackFrame.FunctionName = stackFrame.FunctionName.Substring(sourceCodeEndIndex + 2); | ||
| stackFrame.Line = line; | ||
| stackFrame.Library = parts[0]; | ||
| // after ']' | ||
| int after = srcEnd + 1; | ||
| if (after < stackFrame.FunctionName.Length && stackFrame.FunctionName[after] == ' ') | ||
| { after++; } | ||
| stackFrame.FunctionName = (after < stackFrame.FunctionName.Length) | ||
| ? stackFrame.FunctionName.Substring(after) | ||
| : string.Empty; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| using Backtrace.Unity.Model; | ||
| using Backtrace.Unity.Types; | ||
| using NUnit.Framework; | ||
| using System; | ||
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Reflection; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using UnityEngine.TestTools; | ||
|
|
||
| namespace Backtrace.Unity.Tests.Runtime{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this file is not formatted There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a draft to review the native parsing logic; |
||
| public class NativeParserTests | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we be more specific? I would call it NativeStackTraceParser instead There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure |
||
| { | ||
| private static BacktraceStackFrame Parse(string frame) | ||
| { | ||
| var instance = new BacktraceUnhandledException("msg", ""); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not a valid way to test it. Please follow the parser tests that we already have in the tests. |
||
| var mi = typeof(BacktraceUnhandledException) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is mi? |
||
| .GetMethod("SetNativeStackTraceInformation", | ||
| BindingFlags.NonPublic | BindingFlags.Instance); | ||
| Assert.IsNotNull(mi); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what it tests? |
||
| return (BacktraceStackFrame)mi.Invoke(instance, new object[] { frame }); | ||
| } | ||
|
|
||
| [Test] | ||
| public void SymbolLessModuleLine_ParsesAddressAndLibrary_NoThrow() | ||
| { | ||
| var f = Parse("0x00007ffad7723088 (UnityPlayer)"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is f? |
||
| Assert.AreEqual("0x00007ffad7723088", f.Address); | ||
| Assert.AreEqual("UnityPlayer", f.Library); | ||
| Assert.AreEqual(string.Empty, f.FunctionName); | ||
| Assert.AreEqual(Types.BacktraceStackFrameType.Native, f.StackFrameType); | ||
| } | ||
|
|
||
| [Test] | ||
| public void WithSymbol_ParsesMethod() | ||
| { | ||
| var f = Parse("0x00007ffad7ee3c7d (UnityPlayer) UnityMain"); | ||
| Assert.AreEqual("UnityPlayer", f.Library); | ||
| Assert.AreEqual("UnityMain", f.FunctionName); | ||
| } | ||
|
|
||
| [Test] | ||
| public void UnknownShape_ReturnsRawInFunctionName() | ||
| { | ||
| var f = Parse("nonsense frame with no address"); | ||
| Assert.AreEqual("nonsense frame with no address", f.FunctionName); | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pleaes move it to a separated pull request. in the git history no one expects to see cicd pipeline update in the native stack trace parser pull request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's the plan!