Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- First managed report no longer builds Regex objects (mitigation, not a fix): on one HONOR WOD-LX1 with a Unity 2021.3.45f1 Mono (ARMv7) Release build, 2 of 28 fresh-install cold starts died with SIGSEGV in `GC_mark_from` (libmonobdwgc) while the SDK built its Regex objects for the first managed exception report (managed stack: `RegexCharClass:.cctor` <- `Regex:.ctor` <- `ManagedAddressPairing:.cctor` <- `ExceptionPipeline.Report`). `ManagedAddressPairing` and `ManagedSignatureRecipe` now parse frame locations and shared-library names by hand (`ManagedFrameLocation`), with the same results; the old patterns are kept in the host tests as the reference and compared over real device frames and generated inputs. Not claimed: the suspected Mono GC defect is not fixed, and 0 crashes in 41 fresh-install cold starts afterwards is only suggestive (at a 7% rate, 0 of 41 would still happen about 5% of the time). Other first-use work on the report path (SHA-256 creation, `StackTrace` formatting, JIT) is unchanged.
- iOS SDK 7.0.0 (stable) adopted: `bugsee/spm` and `bugsee/feedback-spm` `7.0.0` (the export pins both to `IOS_SDK_VERSION`; feedback-spm 7.0.0 requires exactly core 7.0.0), the vendored iOS options manifest and `Tests~/iOS/run-tests.sh` move to 7.0.0. Checked against the 7.0.0 and 7.0.0-beta7 headers: the wrapper SPI (`BugseeWrapper`, `BGSWrapperChannel`, `BGSReportContract`, ...) moved out of `Bugsee.h` into `<Bugsee/BugseeSPI.h>` (`Bugsee.SPI`), so the bridge now imports that header and no longer builds against 7.0.0-beta7 (`assemble-local-ios-sdk.sh` now requires 7.0.0 or later and accepts a stable version). New in 7.0.0 and not wired: `+[Bugsee defaultNetworkSanitizer]` (`Bugsee.ApplyDefaultNetworkSanitizer` still returns false on iOS; #267), the public `BugseeReportHandler`, `deleteCollectedDataOnDevice:includingBundles:completion:`, `addBreadcrumb:requiresFiltering:`, and on the wrapper channel `logWithTag:message:level:source:timestamp:` and `currentTimeMillis` (for wrappers that delay lines). Per the 7.0.0 header, `deleteCollectedDataOnDevice:` (and the new includingBundles form) can complete with NO while a report is still being bundled; the wrapper's delete completion passes that result on as before. Behaviour changes found: per the 7.0.0 header and manifest (not measured on a device beyond the keys being accepted and read back), `MaxDataSize` now defaults to 150 (was 50) and is enforced on iOS, and `MaxPendingReports` / `MaxPendingReportAge` (default 30 / 30 days) exist on iOS, so these three are now shared options (`MaxPendingReports` and `MaxPendingReportAge` moved from `AndroidLaunchOptions` to `BugseeLaunchOptions` and are no longer gated off an iOS launch; source compatible); per the 7.0.0 header (not measured on a device), the iOS network, log and breadcrumb filters now drop an event whose decision is not given within 10 s; `Stop` ends a blackout, so `Bugsee.IsBlackout` is false after `Stop` and after the next `Launch` (7.0.0-beta7 kept it; measured on an iPhone XS with the `blackout` scenario). The header's exception, not exercised on a device: a native relaunch restores the blackout its stop ended, so a `Relaunch` started during a blackout keeps it (the device run's `Relaunch` step already showed `IsBlackout` true afterwards), and a stop while nothing is launched ends nothing. Unchanged and re-checked: `LogException` takes only `skipFrames` and the domain, no video-permission API (`ResetVideoCapturePermission` stays a one-time warning), no `ReportHandlerCallbackTimeout` key (the SDK uses 30 s per callback), `CrashHandlerType` and the other iOS-only keys (read back on the device), and `BugseeAttachment` still has no MIME type. `config.data-encryption`: 7.0.0 no longer declares `BugseeOptionConfigDataEncryption` or `BugseeDataEncryptionMode` in its public headers, but the options manifest still lists the key and the binary still has `setDataEncryption:` and `initializeDataEncryption`; on the device the key is accepted and read back by `GetLaunchOptions`, and a launch with `DataEncryption = OnDevice` uploads a report the dashboard shows normally (IUTA-73); that the data is actually encrypted, and `EndToEnd`, are not verified. The local-package workflow now also copies `Sources/BugseeCrashExtension` (the 7.0.0 manifest has that target; without it Xcode failed with `invalid custom path`): `assemble-local-ios-sdk.sh` copies it from `BugseeCrashExtension/Library` and checks every manifest path, and the export's copy takes all of `Sources`. Device run (iPhone XS, iOS 18.7.9, Unity 2022.3.62f3 IL2CPP, staging app IUTA): launch, handled error (IUTA-75), uncaught managed exception (IUTA-74), `TestCrash` (IUTA-77), a native SIGSEGV in `Utils_CUSTOM_ForceCrash` (IUTA-78) and a raised signal (IUTA-79) symbolicated with the uploaded dSYMs, `screenshots` (IUTA-76), and the `vh`, `options`, `filters`, `extras` and `videoperm` scenarios passed.
- IL2CPP handled exceptions without file:line (#263): on one HONOR WOD-LX1 (Android 14, Unity 6000.0.84f1, SDK 7.4.2, staging app AU, default IL2CPP stack trace information) an uncaught coroutine `NullReferenceException` reported through the exception pipeline resolved to `NullRefE2E.cs:96` in a Release build with Debugging symbols plus the il2cpp-linemap, stayed `<0000...>:0` with Public symbols (the same addresses were sent), and stayed unresolved in a Development IL2CPP build, which sent no addresses (zero native IPs observed). The Android build warning for Create symbols.zip Public, Disabled or Debugging-without-Zip now adds that consequence, only for an IL2CPP Release build whose IL2CPP line information is known to be off (`AndroidSymbolPolicy.NeedsNativeLinesForManagedFrames`); never for Mono, Development or IL2CPP line information builds. Docs: Mode B scoped to Android on 6000.0.84f1 and the new section *IL2CPP handled exceptions: what resolves*, which also records two gaps outside this package: Release IL2CPP native crashes stayed unresolved even with Debugging symbols and matching uploads, and a group's page shows its first event's unresolved frames. No runtime behaviour changed.
- Android Mono null references (#233): documentation only. On one device (HONOR WOD-LX1, Android SDK 7.4.2, NDK handler on) the tested Mono builds (2022.3 Development, 6000.0.84f1 Release) produced no native crash; the 6000.0.84f1 Release run's exception arrived on the dashboard as a managed handled error (the 2022.3 run was judged from the device log and the next launch only). Whether Mono raised it through a signal handler or explicit checks is not established, other devices and Unity versions were not checked, and the NDK default is unchanged; see the Null references section of the documentation. The sample's `E2EBuild` gained `BUGSEE_E2E_ARCH` (`armv7`, `arm64`, `both`; the last two are IL2CPP only and rejected for Mono). Addresses #233 (the signal selector is still open).
Expand Down
15 changes: 5 additions & 10 deletions Runtime/WrapperPolicy/ManagedAddressPairing.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Text.RegularExpressions;

namespace Bugsee.WrapperPolicy
{
Expand Down Expand Up @@ -33,12 +32,6 @@ namespace Bugsee.WrapperPolicy
/// </summary>
public static class ManagedAddressPairing
{
// "... in /path/File.cs:123" (a usym entry), or "in <32 hex>:123" (usym entry without a file name).
// Release IL2CPP without a usym file prints "in <32 hex>:0".
static readonly Regex SourceLocation = new Regex(
@"\sin\s+(?<file>.+):(?<line>\d+)\s*$",
RegexOptions.Compiled);

/// <summary>A line of the stack text that separates segments instead of being a frame.</summary>
public static bool IsSeparator(string line)
{
Expand All @@ -49,10 +42,12 @@ public static bool IsSeparator(string line)
public static bool HasSourceLocation(string line)
{
if (string.IsNullOrEmpty(line)) return false;
var m = SourceLocation.Match(line);
if (!m.Success) return false;
// "... in /path/File.cs:123" (a usym entry), or "in <32 hex>:123" (usym entry without a file name).
// Release IL2CPP without a usym file prints "in <32 hex>:0".
string file, digits;
if (!ManagedFrameLocation.TryInLocation(line, out file, out digits)) return false;
int number;
return int.TryParse(m.Groups["line"].Value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out number) && number > 0;
return int.TryParse(digits, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out number) && number > 0;
}

public static string FormatAddress(ulong moduleRelative)
Expand Down
94 changes: 94 additions & 0 deletions Runtime/WrapperPolicy/ManagedFrameLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;

namespace Bugsee.WrapperPolicy
{
/// <summary>
/// Hand-written parsers for the source-location suffix of a managed stack frame and for a shared-library name.
/// They replace four <c>Regex</c> objects (three distinct patterns, one used twice) that were built on the FIRST managed
/// report. On Unity 2021.3 Mono (ARMv7) the stack of a SIGSEGV in <c>GC_mark_from</c> showed that Regex construction
/// (<c>RegexCharClass</c> static constructor) running when the fault happened. Behaviour is identical to the patterns they replace, which
/// the host tests keep as the reference and compare over a generated corpus:
/// <list type="bullet">
/// <item><c>\sin\s+(?&lt;file&gt;.+):(?&lt;line&gt;\d+)\s*$</c> (<see cref="TryInLocation"/>)</item>
/// <item><c>\(at\s+(?&lt;file&gt;.+):(?&lt;line&gt;\d+)\)\s*$</c> (<see cref="TryAtLocation"/>)</item>
/// <item><c>^lib[^\s/()]*\.so(?![A-Za-z0-9_])</c> (<see cref="IsSharedLibrary"/>)</item>
/// </list>
/// Nothing here may allocate more than the two result substrings or touch System.Text.RegularExpressions.
/// </summary>
static class ManagedFrameLocation
{
/// <summary>"... in FILE:LINE" at the end of the text (trailing whitespace allowed); file and the digits of the line.</summary>
public static bool TryInLocation(string text, out string file, out string line)
{
return TryLocation(text, false, out file, out line);
}

/// <summary>"... (at FILE:LINE)" at the end of the text (trailing whitespace allowed); file and the digits of the line.</summary>
public static bool TryAtLocation(string text, out string file, out string line)
{
return TryLocation(text, true, out file, out line);
}

// Regex \s and \d: whitespace as char.IsWhiteSpace, decimal digits (any script) as char.IsDigit.
static bool Space(char c) { return char.IsWhiteSpace(c); }

static bool TryLocation(string text, bool parenthesized, out string file, out string line)
{
file = null;
line = null;
if (string.IsNullOrEmpty(text)) return false;

// Tail: ":" digits [")"] whitespace* end. The file is greedy, so the colon is the one before the final digit run.
var end = text.Length;
while (end > 0 && Space(text[end - 1])) end--;
if (parenthesized)
{
if (end == 0 || text[end - 1] != ')') return false;
end--;
}
var digits = end;
while (digits > 0 && char.IsDigit(text[digits - 1])) digits--;
if (digits == end || digits == 0 || text[digits - 1] != ':') return false;
var colon = digits - 1;
if (colon == 0) return false; // no room for the opener and a file

// Head, leftmost first: ws "in" ws+ (or "(at" ws+) before at least one file char; the file has no line feed.
var lastLineFeed = text.LastIndexOf('\n', colon - 1);
var opener = parenthesized ? "(at" : "in";
var skip = parenthesized ? 0 : 1; // the leading whitespace of the "in" form
for (var i = 0; ; i++)
{
var wordStart = i + skip;
var after = wordStart + opener.Length;
if (after + 1 >= colon) return false; // needs whitespace and at least one file character
if (parenthesized ? text[i] != '(' : !Space(text[i])) continue;
if (string.CompareOrdinal(text, wordStart, opener, 0, opener.Length) != 0 || !Space(text[after])) continue;
var wsEnd = after;
while (wsEnd < colon && Space(text[wsEnd])) wsEnd++;
var start = Math.Min(wsEnd, colon - 1); // \s+ is greedy but must leave one character for the file
if (start <= lastLineFeed) continue;
file = text.Substring(start, colon - start);
line = text.Substring(digits, end - digits);
return true;
}
}

/// <summary>"libXXX.so" at the start: no whitespace, slash or parenthesis before ".so", and no letter, digit or underscore after it.</summary>
public static bool IsSharedLibrary(string token)
{
if (token == null || !token.StartsWith("lib", StringComparison.Ordinal)) return false;
for (var p = 3; p < token.Length; p++)
{
var c = token[p];
if (Space(c) || c == '/' || c == '(' || c == ')') return false;
if (c == '.' && p + 3 <= token.Length && token[p + 1] == 's' && token[p + 2] == 'o')
{
if (p + 3 == token.Length) return true;
var n = token[p + 3];
if (!((n >= 'A' && n <= 'Z') || (n >= 'a' && n <= 'z') || (n >= '0' && n <= '9') || n == '_')) return true;
}
}
return false;
}
}
}
11 changes: 11 additions & 0 deletions Runtime/WrapperPolicy/ManagedFrameLocation.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 5 additions & 12 deletions Runtime/WrapperPolicy/ManagedSignatureRecipe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace Bugsee.WrapperPolicy
{
Expand Down Expand Up @@ -35,11 +34,6 @@ public static class ManagedSignatureRecipe
// Wrapper namespaces are engine; the sample app's own code lives in Bugsee.Sample and counts as user code.
const string SampleNamespace = "Bugsee.Sample.";

// libunity.so, libil2cpp.so, libmono-2.0.so, optionally followed by "(+0x...)": the ".so" must end the name.
static readonly Regex SharedLibrary = new Regex(@"^lib[^\s/()]*\.so(?![A-Za-z0-9_])", RegexOptions.Compiled);
static readonly Regex AtLocation = new Regex(@"\(at\s+(?<file>.+):(?<line>\d+)\)\s*$", RegexOptions.Compiled);
static readonly Regex InLocation = new Regex(@"\sin\s+(?<file>.+):(?<line>\d+)\s*$", RegexOptions.Compiled);

/// <summary><c>Type.Method</c> of a frame line ("at " prefix, arguments, offsets and location dropped); "" when none.</summary>
public static string MethodToken(string frame)
{
Expand All @@ -63,7 +57,7 @@ public static bool IsEngineFrame(string frame)
if (token.Length == 0 || token[0] == '(') return true; // "(wrapper managed-to-native)" and the like
if (token[0] == '#' || token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return true; // native lines of a Unity log: "#00 pc ...", "0x... in UnityFramework"
// A line that starts with a shared library name ("libunity.so(+0x1a2b)"); never a substring of a managed frame's class or path.
if (SharedLibrary.IsMatch(token)) return true;
if (ManagedFrameLocation.IsSharedLibrary(token)) return true; // libunity.so, libil2cpp.so, libmono-2.0.so, optionally followed by "(+0x...)": the ".so" must end the name
if (token.StartsWith(SampleNamespace, StringComparison.Ordinal)) return false;
foreach (var p in EnginePrefixes)
{
Expand All @@ -81,15 +75,14 @@ public static bool TryFileLine(string frame, out string file, out int line)
file = null;
line = 0;
if (string.IsNullOrEmpty(frame)) return false;
var m = AtLocation.Match(frame);
if (!m.Success) m = InLocation.Match(frame);
if (!m.Success) return false;
if (!int.TryParse(m.Groups["line"].Value, System.Globalization.NumberStyles.Integer,
string path, digits;
if (!ManagedFrameLocation.TryAtLocation(frame, out path, out digits) && !ManagedFrameLocation.TryInLocation(frame, out path, out digits)) return false;
if (!int.TryParse(digits, System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture, out line) || line <= 0)
{
return false;
}
var path = m.Groups["file"].Value.Trim();
path = path.Trim();
var cut = Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\'));
var name = cut >= 0 ? path.Substring(cut + 1) : path;
if (name.Length == 0 || name[0] == '<') return false;
Expand Down
Loading
Loading