From b0bcb361b54d82cf729847ff64107b3cf01704c9 Mon Sep 17 00:00:00 2001 From: melekr Date: Wed, 8 Oct 2025 00:53:13 -0400 Subject: [PATCH 1/4] Prevent crash when parsing native frames without symbols Add bounds checks in SetNativeStackTraceInformation Add NativeParserTests to validate safe native frame parsing --- Runtime/Model/BacktraceUnhandledException.cs | 115 ++++++++++++------- Tests/Runtime/NativeParserTests.cs | 51 ++++++++ Tests/Runtime/NativeParserTests.cs.meta | 11 ++ 3 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 Tests/Runtime/NativeParserTests.cs create mode 100644 Tests/Runtime/NativeParserTests.cs.meta diff --git a/Runtime/Model/BacktraceUnhandledException.cs b/Runtime/Model/BacktraceUnhandledException.cs index cb34c611..0309dd26 100644 --- a/Runtime/Model/BacktraceUnhandledException.cs +++ b/Runtime/Model/BacktraceUnhandledException.cs @@ -136,13 +136,13 @@ private List ConvertStackFrames(IEnumerable 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); + 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) } /// - /// 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. /// - /// Native stack frame - /// Backtrace stack frame + /// Raw native stack frame line to parse. + /// Parsed Backtrace stack frame containing address, library, function name, and optional line number. private BacktraceStackFrame SetNativeStackTraceInformation(string frameString) { var stackFrame = new BacktraceStackFrame { - StackFrameType = Types.BacktraceStackFrameType.Native + StackFrameType = Types.BacktraceStackFrameType.Native, + FunctionName = string.Empty, + Line = 0 }; - // 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; } } diff --git a/Tests/Runtime/NativeParserTests.cs b/Tests/Runtime/NativeParserTests.cs new file mode 100644 index 00000000..889a227a --- /dev/null +++ b/Tests/Runtime/NativeParserTests.cs @@ -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{ + public class NativeParserTests + { + private static BacktraceStackFrame Parse(string frame) + { + var instance = new BacktraceUnhandledException("msg", ""); + var mi = typeof(BacktraceUnhandledException) + .GetMethod("SetNativeStackTraceInformation", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(mi); + return (BacktraceStackFrame)mi.Invoke(instance, new object[] { frame }); + } + + [Test] + public void SymbolLessModuleLine_ParsesAddressAndLibrary_NoThrow() + { + var f = Parse("0x00007ffad7723088 (UnityPlayer)"); + 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); + } + } +} diff --git a/Tests/Runtime/NativeParserTests.cs.meta b/Tests/Runtime/NativeParserTests.cs.meta new file mode 100644 index 00000000..de174c46 --- /dev/null +++ b/Tests/Runtime/NativeParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a99b0743e15f2473894961191792e63d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From a1797acf143a106f02ac4bc72ed4fd50f2dd7767 Mon Sep 17 00:00:00 2001 From: melekr Date: Wed, 8 Oct 2025 01:22:10 -0400 Subject: [PATCH 2/4] Update workflow unityVersion --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cd07616..8f891e9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: projectPath: - test-package unityVersion: - - 2019.4.40f1 + - 2022.3.56f1 targetPlatform: - StandaloneOSX - StandaloneWindows From a3ee28ab783d493671ca6050bc072136585371e4 Mon Sep 17 00:00:00 2001 From: melekr Date: Wed, 8 Oct 2025 01:47:56 -0400 Subject: [PATCH 3/4] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f891e9b..55ef2ded 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,5 +48,5 @@ jobs: unityVersion: ${{ matrix.unityVersion }} targetPlatform: ${{ matrix.targetPlatform }} projectPath: ${{ matrix.projectPath }}/ - allowDirtyBuild: true + testMode: editmode \ No newline at end of file From 236f94dbd10ebff748cb2f4d220125663899fb0b Mon Sep 17 00:00:00 2001 From: melekr Date: Wed, 8 Oct 2025 01:49:04 -0400 Subject: [PATCH 4/4] Update build.yml --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55ef2ded..dce1b479 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,5 +48,6 @@ jobs: unityVersion: ${{ matrix.unityVersion }} targetPlatform: ${{ matrix.targetPlatform }} projectPath: ${{ matrix.projectPath }}/ + allowDirtyBuild: true testMode: editmode \ No newline at end of file