Skip to content

Prevent NREX when instantiating Java.Lang.StackTraceElement #8795

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

Merged
merged 8 commits into from
Mar 17, 2024
87 changes: 83 additions & 4 deletions src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;

using StackTraceElement = Java.Lang.StackTraceElement;

Expand Down Expand Up @@ -37,6 +39,81 @@ public static JavaProxyThrowable Create (Exception innerException)
return proxy;
}

(int lineNumber, string? methodName, string? className) GetFrameInfo (StackFrame? managedFrame, MethodBase? managedMethod)
{
string? methodName = null;
string? className = null;

if (managedFrame == null) {
if (managedMethod != null) {
methodName = managedMethod.Name;
className = managedMethod.DeclaringType?.FullName;
}

return (-1, methodName, className);
}

int lineNumber = -1;
lineNumber = managedFrame.GetFileLineNumber ();
if (lineNumber == 0) {
// -2 means it's a native frame
lineNumber = managedFrame.HasNativeImage () ? -2 : -1;
}

if (managedMethod != null) {
// If we have no line number information and if it's a managed frame, add the
// IL offset.
if (lineNumber == -1 && managedFrame.HasILOffset ()) {
methodName = $"{managedMethod.Name} + 0x{managedFrame.GetILOffset():x}";
} else {
methodName = managedMethod.Name;
}

return (lineNumber, methodName, managedMethod.DeclaringType?.FullName);
}

string frameString = managedFrame.ToString ();
var sb = new StringBuilder ();

// We take the part of the returned string that stretches from the beginning to the first space character
// and treat it as the method name.
// https://github.com/dotnet/runtime/blob/18c3ad05c3fc127c3b7f37c49bc350bf7f8264a0/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/DeveloperExperience/DeveloperExperience.cs#L15-L55
int pos = frameString.IndexOf (' ');
string? fullName = null;
if (pos > 1) {
fullName = frameString.Substring (0, pos);
}

if (!String.IsNullOrEmpty (fullName) && (pos = fullName.LastIndexOf ('.')) >= 1) {
className = pos + 1 < fullName.Length ? fullName.Substring (pos + 1) : null;
fullName = fullName.Substring (0, pos);
}

if (!String.IsNullOrEmpty (fullName)) {
sb.Append (fullName);
} else if (managedFrame.HasNativeImage ()) {
// We have no name, so we'll put the native IP
nint nativeIP = managedFrame.GetNativeIP ();
sb.Append (CultureInfo.InvariantCulture, $"Native 0x{nativeIP:x}");
}

if (sb.Length > 0) {
// We will also append information native offset information, if available and only if we
// have recorded any previous information, since the offset without context is useless.
int nativeOffset = managedFrame.GetNativeOffset ();
if (nativeOffset != StackFrame.OFFSET_UNKNOWN) {
sb.Append (" + ");
sb.Append (CultureInfo.InvariantCulture, $"0x{nativeOffset:x}");
}
}

if (sb.Length > 0) {
methodName = sb.ToString ();
}

return (lineNumber, methodName, className);
}

void TranslateStackTrace ()
{
// FIXME: https://github.com/xamarin/xamarin-android/issues/8724
Expand All @@ -61,20 +138,22 @@ void TranslateStackTrace ()
// ..but ignore
}


StackFrame[] frames = trace.GetFrames ();
int nElements = frames.Length + (javaTrace?.Length ?? 0);
StackTraceElement[] elements = new StackTraceElement[nElements];

const string Unknown = "Unknown";
for (int i = 0; i < frames.Length; i++) {
StackFrame managedFrame = frames[i];
MethodBase? managedMethod = StackFrameGetMethod (managedFrame);

// https://developer.android.com/reference/java/lang/StackTraceElement?hl=en#StackTraceElement(java.lang.String,%20java.lang.String,%20java.lang.String,%20int)
(int lineNumber, string? methodName, string? declaringClass) = GetFrameInfo (managedFrame, managedMethod);
var throwableFrame = new StackTraceElement (
declaringClass: managedMethod?.DeclaringType?.FullName,
methodName: managedMethod?.Name,
declaringClass: declaringClass ?? Unknown,
methodName: methodName ?? Unknown,
fileName: managedFrame?.GetFileName (),
lineNumber: managedFrame?.GetFileLineNumber () ?? -1
lineNumber: lineNumber
);

elements[i] = throwableFrame;
Expand Down
29 changes: 20 additions & 9 deletions tests/Mono.Android-Tests/System/ExceptionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public void InnerExceptionIsSet ()
ex = e;
}

using (Java.Lang.Throwable proxy = CreateJavaProxyThrowable (ex))
using (var source = new Java.Lang.Throwable ("detailMessage", proxy))
using (var alias = new Java.Lang.Throwable (source.Handle, JniHandleOwnership.DoNotTransfer)) {
CompareStackTraces (ex, proxy);
Assert.AreEqual ("detailMessage", alias.Message);
Assert.AreSame (ex, alias.InnerException);
}
using Java.Lang.Throwable proxy = CreateJavaProxyThrowable (ex);
using var source = new Java.Lang.Throwable ("detailMessage", proxy);
using var alias = new Java.Lang.Throwable (source.Handle, JniHandleOwnership.DoNotTransfer);

CompareStackTraces (ex, proxy);
Assert.AreEqual ("detailMessage", alias.Message);
Assert.AreSame (ex, alias.InnerException);
}

void CompareStackTraces (Exception ex, Java.Lang.Throwable throwable)
Expand All @@ -61,10 +61,21 @@ void CompareStackTraces (Exception ex, Java.Lang.Throwable throwable)
var mf = managedFrames[i];
var jf = javaFrames[i];

Assert.AreEqual (mf.GetMethod ()?.Name, jf.MethodName, $"Frame {i}: method names differ");
// Unknown line locations are -1 on the Java side if they're managed, -2 if they're native
int managedLine = mf.GetFileLineNumber ();
if (managedLine == 0) {
managedLine = mf.HasNativeImage () ? -2 : -1;
}

if (managedLine > 0) {
Assert.AreEqual (mf.GetMethod ()?.Name, jf.MethodName, $"Frame {i}: method names differ");
} else {
string managedMethodName = mf.GetMethod ()?.Name ?? String.Empty;
Assert.IsTrue (jf.MethodName.StartsWith ($"{managedMethodName} + 0x"), $"Frame {i}: method name should start with: '{managedMethodName} + 0x'");
}
Assert.AreEqual (mf.GetMethod ()?.DeclaringType.FullName, jf.ClassName, $"Frame {i}: class names differ");
Assert.AreEqual (mf.GetFileName (), jf.FileName, $"Frame {i}: file names differ");
Assert.AreEqual (mf.GetFileLineNumber (), jf.LineNumber, $"Frame {i}: line numbers differ");
Assert.AreEqual (managedLine, jf.LineNumber, $"Frame {i}: line numbers differ");
}
}
}
Expand Down