Repository navigation
Expand file tree
/
Copy pathIl2CppEventProcessor.cs
More file actions
415 lines (366 loc) · 17.3 KB
/
Copy pathIl2CppEventProcessor.cs
File metadata and controls
415 lines (366 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Sentry.Extensibility;
using Sentry.Protocol;
using Sentry.Unity.Integrations;
using UnityEngine;
using Application = UnityEngine.Application;
namespace Sentry.Unity;
internal class UnityIl2CppEventExceptionProcessor : ISentryEventExceptionProcessor
{
private static SentryUnityOptions Options = null!; // private static will be initialized in the constructor
private static ISentryUnityInfo UnityInfo = null!; // private static will be initialized in the constructor
private readonly Il2CppMethods _il2CppMethods;
public UnityIl2CppEventExceptionProcessor(SentryUnityOptions options)
{
Options = options;
UnityInfo = options.UnityInfo;
// We're throwing here but this should never happen. We're validating UnityInfo before adding the processor.
_il2CppMethods = UnityInfo.Il2CppMethods ?? throw new ArgumentNullException(nameof(UnityInfo.Il2CppMethods),
"Unity IL2CPP methods are not available.");
Options.SdkIntegrationNames.Add("IL2CPPLineNumbers");
}
public void Process(Exception incomingException, SentryEvent sentryEvent)
{
Options.LogDebug("Running Unity IL2CPP event exception processor on: Event {0}", sentryEvent.EventId);
var sentryExceptions = sentryEvent.SentryExceptions;
if (sentryExceptions == null)
{
return;
}
var exceptions = EnumerateChainedExceptions(incomingException);
var usedImages = new HashSet<DebugImage>();
// Unity usually produces stack traces with relative offsets in the GameAssembly library.
// However, at least on Unity 2020 built Windows player, the offsets seem to be absolute.
// Therefore, we try to determine which one it is, depending on whether they match the loaded libraries.
// In case they don't we update the offsets to match the GameAssembly library.
foreach (var (sentryException, exception) in sentryExceptions.Zip(exceptions, (se, ex) => (se, ex)))
{
if (sentryException.Mechanism?.Synthetic is true)
{
// Skip synthetic exceptions since they have no native counterpart
continue;
}
var sentryStacktrace = sentryException.Stacktrace;
if (sentryStacktrace == null)
{
// We will only augment an existing stack trace with native
// instructions, so with no stack trace, there is nothing to do
continue;
}
sentryStacktrace.AddressAdjustment =
Application.platform == RuntimePlatform.Android
? InstructionAddressAdjustment.None
: InstructionAddressAdjustment.All;
var nativeStackTrace = GetNativeStackTrace(exception);
Options.LogDebug("NativeStackTrace Image: '{0}' (UUID: {1})", nativeStackTrace.ImageName, nativeStackTrace.ImageUuid);
// Unity by definition only builds a single library which we add once to our list of debug images.
// We use this when we encounter stack frames with relative addresses.
// We want to use an address that is definitely outside of any address range used by real libraries.
// Canonical addresses on x64 leave a gap in the middle of the address space, which is unused.
// This is a range of addresses that we should be able to safely use.
// See https://en.wikipedia.org/wiki/X86-64#Virtual_address_space_details
var mainLibOffset = long.MaxValue;
DebugImage? mainLibImage = null;
var mainImageUuid = NormalizeUuid(nativeStackTrace.ImageUuid);
if (mainImageUuid is null && !string.IsNullOrEmpty(nativeStackTrace.ImageName))
{
// il2cpp only scans the first PT_NOTE segment when reading the ELF build ID. On x86_64 with NDK r23 the
// notes end up in two segments and the build ID lands in the one il2cpp skips. sentry-native reports
// the image just fine, so we look it up by the name il2cpp gave us.
var imageByName = DebugImagesSorted.Value
.Find(info => string.Equals(info.Image.CodeFile, nativeStackTrace.ImageName))?.Image;
mainImageUuid = NormalizeUuid(imageByName?.DebugId);
if (mainImageUuid is not null)
{
mainLibImage = imageByName;
Options.LogDebug("Unity reported no main image UUID. Resolved '{0}' from the debug images instead.",
mainImageUuid);
}
}
// TODO do we really want to continue if these two don't match?
// Wouldn't it cause invalid frame info?
var nativeLen = nativeStackTrace.Frames.Length;
var eventLen = sentryStacktrace.Frames.Count;
if (nativeLen != eventLen)
{
Options.LogWarning(
"Native and sentry stack trace lengths don't match '({0} != {1})' - this may cause invalid stack traces.",
nativeLen, eventLen);
}
var len = Math.Min(eventLen, nativeLen);
for (var i = 0; i < len; i++)
{
// The sentry stack trace is sorted parent->child (caller->callee),
// whereas the native stack trace is sorted from callee to caller.
var frame = sentryStacktrace.Frames[i];
var nativeFrame = nativeStackTrace.Frames[nativeLen - 1 - i];
// TODO should we do this for all addresses or only relative ones?
// If the former, we should also update `frame.InstructionAddress` down below.
var instructionAddress = nativeFrame.ToInt64();
// We cannot determine whether this frame is a main library frame just from the address
// because even relative address on the frame may correspond to an absolute address of a loaded library.
// Therefore, if the frame package matches known prefixes, we assume it's a GameAssembly frame.
var isMainLibFrame = frame.Package is not null && (
frame.Package.StartsWith("UnityEngine.", StringComparison.InvariantCultureIgnoreCase) ||
frame.Package.StartsWith("Assembly-CSharp", StringComparison.InvariantCultureIgnoreCase)
);
string? notes = null;
DebugImage? image = null;
bool? isRelativeAddress = null;
if (!isMainLibFrame)
{
image = FindDebugImageContainingAddress(instructionAddress);
if (image is null)
{
isRelativeAddress = true;
notes = "because it looks like a relative address.";
// falls through to the next `if (image is null)`
}
else
{
isRelativeAddress = false;
notes = "because it looks like an absolute address inside the range of this debug image.";
}
}
if (image is null)
{
if (mainImageUuid is null)
{
Options.LogWarning("Couldn't process stack trace - main image UUID reported as NULL by Unity");
continue;
}
// First, try to find the image among the loaded ones, otherwise create a dummy one.
mainLibImage ??= DebugImagesSorted.Value.Find((info) => string.Equals(NormalizeUuid(info.Image.DebugId), mainImageUuid))?.Image;
mainLibImage ??= new DebugImage
{
Type = GetPlatformDebugImageType(),
// NOTE: il2cpp in some circumstances does not return a correct `ImageName`.
// A null/missing `CodeFile` however would lead to a processing error in sentry.
// Since the code file is not strictly necessary for processing, we just fall back to
// a sentinel value here.
CodeFile = string.IsNullOrEmpty(nativeStackTrace.ImageName) ? "GameAssembly.fallback" : nativeStackTrace.ImageName,
DebugId = mainImageUuid,
ImageAddress = mainLibOffset,
};
image = mainLibImage;
if (isMainLibFrame)
{
notes ??= $"based on frame package name ({frame.Package}).";
}
}
var imageAddress = image.ImageAddress!.Value;
isRelativeAddress ??= instructionAddress < imageAddress;
if (isRelativeAddress is true)
{
// Shift the instruction address to be absolute.
instructionAddress += imageAddress;
frame.InstructionAddress = instructionAddress;
}
// sanity check that the instruction fits inside the range
var logLevel = SentryLevel.Debug;
if (image.ImageSize is not null)
{
if (instructionAddress < imageAddress || instructionAddress > imageAddress + image.ImageSize)
{
logLevel = SentryLevel.Warning;
notes ??= ".";
notes += " However, the instruction address falls out of the range of the debug image.";
}
}
Options.Log(logLevel, "Stack frame '{0}' at {1:X8} (originally {2:X8}) belongs to {3} {4}",
null, frame.Function, instructionAddress, nativeFrame.ToInt64(), image.CodeFile, notes ?? "");
_ = usedImages.Add(image);
}
}
sentryEvent.DebugImages ??= new List<DebugImage>();
sentryEvent.DebugImages.AddRange(usedImages);
}
// Normalizes Debug Image UUID so that we can compare the ones coming from
// native (contains dashes, all lower-case) & what Unity gives us (no dashes, uppercase).
// On Linux, the image also has shorter UUID coming from Unity, e.g. 3028cb80b0712541,
// while native image UUID we get is 3028cb80-b071-2541-0000-000000000000.
internal static string? NormalizeUuid(string? value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
value = value!.ToLowerInvariant();
value = value.Replace("-0000-000000000000", "");
return value.Replace("-", "");
}
private class DebugImageInfo
{
public readonly DebugImage Image;
public readonly long? StartAddress;
public readonly long? EndAddress;
public DebugImageInfo(DebugImage image)
{
Image = image;
StartAddress = image.ImageAddress!.Value;
EndAddress = StartAddress + image.ImageSize!.Value;
}
public bool ContainsAddress(long address) => StartAddress <= address && address <= EndAddress;
}
private static readonly Lazy<List<DebugImageInfo>> DebugImagesSorted = new(() =>
{
var result = new List<DebugImageInfo>();
// Only on platforms where we actually use sentry-native.
if (IsPlatformSupportedBySentryNative() && Options.IsNativeSupportEnabled())
{
var nativeDebugImages = Options.NativeDebugImageProvider?.GetDebugImages()
?? Enumerable.Empty<DebugImage>();
foreach (var image in nativeDebugImages)
{
if (image.ImageSize is null)
{
Options.LogDebug(
"Skipping debug image '{0}' (CodeId {1} | DebugId: {2}) because its size is NULL",
image.CodeFile, image.CodeId, image.DebugId);
continue;
}
var info = new DebugImageInfo(image);
var i = 0;
for (; i < result.Count; i++)
{
if (info.StartAddress < result[i].StartAddress)
{
// insert at index `i`, all the rest have a larger start address
break;
}
}
result.Insert(i, info);
Options.Log(SentryLevel.Debug,
"Found debug image '{0}' (CodeId {1} | DebugId: {2}) with addresses between {3:X8} and {4:X8}",
null, image.CodeFile, image.CodeId, image.DebugId, info.StartAddress, info.EndAddress);
}
}
return result;
});
private static DebugImage? FindDebugImageContainingAddress(long instructionAddress)
{
var list = DebugImagesSorted.Value;
// Manual binary search implementation on "value in range".
var lowerBound = 0;
var upperBound = list.Count - 1;
while (lowerBound <= upperBound)
{
var mid = (lowerBound + upperBound) / 2;
var info = list[mid];
if (info.StartAddress <= instructionAddress)
{
if (instructionAddress <= info.EndAddress)
{
return info.Image;
}
lowerBound = mid + 1;
}
else
{
upperBound = mid - 1;
}
}
return null;
}
// This is the same logic as `MainExceptionProcessor` uses to create the `SentryEvent.SentryExceptions` list.
// It yields chained Exceptions in innermost to outer Exception order.
private IEnumerable<Exception> EnumerateChainedExceptions(Exception exception)
{
if (exception is AggregateException ae)
{
foreach (var inner in ae.InnerExceptions.SelectMany(EnumerateChainedExceptions))
{
yield return inner;
}
}
else if (exception.InnerException != null)
{
foreach (var inner in EnumerateChainedExceptions(exception.InnerException))
{
yield return inner;
}
}
yield return exception;
}
private NativeStackTrace GetNativeStackTrace(Exception e)
{
// Create a `GCHandle` for the exception, which we can then use to
// essentially get a pointer to the underlying `Il2CppException` C++ object.
var gch = GCHandle.Alloc(e);
// The `il2cpp_native_stack_trace` allocates and writes the native
// instruction pointers to the `addresses`/`numFrames` out-parameters.
var addresses = IntPtr.Zero;
try
{
var gchandle = GCHandle.ToIntPtr(gch);
var addr = _il2CppMethods.Il2CppGcHandleGetTarget(gchandle);
var numFrames = 0;
string? imageUuid = null;
string? imageName = null;
_il2CppMethods.Il2CppNativeStackTrace(addr, out addresses, out numFrames, out imageUuid, out imageName);
// Convert the C-Array to a managed "C#" Array, and free the underlying memory.
var frames = new IntPtr[numFrames];
Marshal.Copy(addresses, frames, 0, numFrames);
return new NativeStackTrace
{
Frames = frames,
ImageUuid = imageUuid,
ImageName = imageName,
};
}
finally
{
// We are done with the `GCHandle`.
gch.Free();
if (addresses != IntPtr.Zero)
{
_il2CppMethods.Il2CppFree(addresses);
}
}
}
private static bool IsPlatformSupportedBySentryNative(RuntimePlatform? platform = null)
{
platform ??= ApplicationAdapter.Instance.Platform;
return platform
is RuntimePlatform.Android
or RuntimePlatform.LinuxPlayer
or RuntimePlatform.LinuxServer
or RuntimePlatform.WindowsPlayer
or RuntimePlatform.WindowsServer
or RuntimePlatform.GameCoreXboxSeries
or RuntimePlatform.GameCoreXboxOne
or RuntimePlatform.PS5;
}
private string GetPlatformDebugImageType(RuntimePlatform? platform = null)
{
platform ??= ApplicationAdapter.Instance.Platform;
switch (platform)
{
case RuntimePlatform.Android:
case RuntimePlatform.LinuxPlayer:
case RuntimePlatform.LinuxServer:
case RuntimePlatform.PS5:
return "elf";
case RuntimePlatform.IPhonePlayer:
case RuntimePlatform.OSXPlayer:
case RuntimePlatform.OSXServer:
return "macho";
case RuntimePlatform.WindowsPlayer:
case RuntimePlatform.WindowsServer:
case RuntimePlatform.GameCoreXboxSeries:
case RuntimePlatform.GameCoreXboxOne:
return "pe";
default:
return "unknown";
}
}
}
internal class NativeStackTrace
{
public IntPtr[] Frames { get; set; } = Array.Empty<IntPtr>();
public string? ImageUuid { get; set; }
public string? ImageName { get; set; }
}