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
37 changes: 37 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,40 @@ jobs:
unzip -Z1 "${app_bundles[0]}" > "$entries"
grep -Eq '(^|/)lib/arm64-v8a/libil2cpp\.so$' "$entries"
grep -Eq '(^|/)lib/arm64-v8a/libbacktrace-native\.so$' "$entries"

dex_entries=()
while IFS= read -r entry; do
if [[ "$entry" =~ ^base/dex/classes([0-9]+)?\.dex$ ]]; then
dex_entries+=("$entry")
fi
done < "$entries"

if (( ${#dex_entries[@]} == 0 )); then
echo "Error: App Bundle contains no base/dex/classes*.dex entries." >&2
exit 1
fi

dex_dir="$RUNNER_TEMP/backtrace-aab-dex"
mkdir -p "$dex_dir"

handler_class='backtraceio/library/nativeCalls/BacktraceCrashHandler'
handler_descriptor="L${handler_class};"
handler_found=false

for dex_index in "${!dex_entries[@]}"; do
dex_entry="${dex_entries[$dex_index]}"
dex_file="$dex_dir/classes-$dex_index.dex"
unzip -p "${app_bundles[0]}" "$dex_entry" > "$dex_file"

if grep -aFq -- "$handler_descriptor" "$dex_file"; then
handler_found=true
break
fi
done

if [[ "$handler_found" != true ]]; then
echo "Error: App Bundle DEX files do not contain ${handler_class}." >&2
printf 'Searched DEX entries:\n' >&2
printf ' %s\n' "${dex_entries[@]}" >&2
exit 1
fi
9 changes: 8 additions & 1 deletion Runtime/BacktraceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ public bool SetAttribute(string key, string value)
AttributeProvider[key] = value;
if (_nativeClient != null)
{
_nativeClient.SetAttribute(key, value);
NativeAttributeLifecycle.TrySetAttribute(
() => _nativeClient.SetAttribute(key, value),
NativeAttributeLifecycle.NativeAttributeFailureCode,
warning => Debug.LogWarning(warning));
}
return true;
}
Expand Down Expand Up @@ -368,6 +371,10 @@ internal INativeClient NativeClient
{
return _nativeClient;
}
set
{
_nativeClient = value;
}
}

public bool EnablePerformanceStatistics
Expand Down
72 changes: 41 additions & 31 deletions Runtime/Native/Android/AndroidLoadedLibraryPath.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#if UNITY_ANDROID
#if UNITY_ANDROID || UNITY_EDITOR
using System;
#if UNITY_ANDROID
using System.Runtime.InteropServices;
#endif

namespace Backtrace.Unity.Runtime.Native.Android
{
Expand All @@ -13,6 +15,7 @@ namespace Backtrace.Unity.Runtime.Native.Android
/// </summary>
internal static class AndroidLoadedLibraryPath
{
#if UNITY_ANDROID
// RTLD_LAZY has the same value on 32- and 64-bit bionic.
// RTLD_NOW (2 on LP64) must NOT be used here: on LP32 bionic the value 2 means RTLD_GLOBAL,
// which would irreversibly promote every exported symbol of the already-loaded library into the global group and let later dlopen'ed libraries bind against them.
Expand Down Expand Up @@ -50,44 +53,51 @@ private struct DlInfo
/// </summary>
internal static string TryGet()
{
try
return TryGet(GetLoadedLibraryPath);
}

private static string GetLoadedLibraryPath()
{
lock (HandleLock)
{
lock (HandleLock)
if (_libraryHandle == IntPtr.Zero)
{
if (_libraryHandle == IntPtr.Zero)
{
_libraryHandle = DlOpen(NativeLibraryName, RtldLazy);
}
if (_libraryHandle == IntPtr.Zero)
{
return null;
}

IntPtr anchor = DlSym(_libraryHandle, AnchorSymbol);
if (anchor == IntPtr.Zero)
{
return null;
}
_libraryHandle = DlOpen(NativeLibraryName, RtldLazy);
}
if (_libraryHandle == IntPtr.Zero)
{
return null;
}

DlInfo information;
if (DlAddr(anchor, out information) == 0 || information.FileName == IntPtr.Zero)
{
return null;
}
IntPtr anchor = DlSym(_libraryHandle, AnchorSymbol);
if (anchor == IntPtr.Zero)
{
return null;
}

string path = Marshal.PtrToStringAnsi(information.FileName);
return string.IsNullOrEmpty(path) ? null : path;
DlInfo information;
if (DlAddr(anchor, out information) == 0 || information.FileName == IntPtr.Zero)
{
return null;
}

return Marshal.PtrToStringAnsi(information.FileName);
}
catch (DllNotFoundException)
{
return null;
}
catch (EntryPointNotFoundException)
}
#endif

/// <summary>
/// Returns a non-empty path supplied by the provider, or null when the provider cannot produce one.
/// The provider boundary is deliberately broad because linker metadata is optional and fallback resolution must continue after any managed failure.
/// </summary>
internal static string TryGet(Func<string> pathProvider)
{
try
{
return null;
string path = pathProvider();
return string.IsNullOrEmpty(path) ? null : path;
}
catch (SEHException)
catch (Exception)
{
return null;
}
Expand Down
68 changes: 41 additions & 27 deletions Runtime/Native/Android/AndroidNativeInitialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
namespace Backtrace.Unity.Runtime.Native.Android
{
/// <summary>
/// Coordinates native-backend activation and the managed setup that follows it.
/// The activation callback is invoked before JNI cleanup,
/// a cleanup failure after a successful native initialization still rolls the backend back.
/// Coordinates native-bridge completion and the managed setup that follows it.
/// The completion callback is invoked before JNI cleanup so a false result or any later
/// cleanup or setup failure can roll back possible native side effects.
/// </summary>
internal static class AndroidNativeInitialization
{
Expand All @@ -29,48 +29,62 @@ internal static bool Execute(
throw new ArgumentNullException("rollback");
}

bool backendActive = false;
bool nativeBridgeCompleted = false;
try
{
bool initialized = initialize(() => backendActive = true);
bool initialized = initialize(() => nativeBridgeCompleted = true);
if (!initialized)
{
if (nativeBridgeCompleted)
{
TryRollback(rollback, reportRollbackFailure);
}

return false;
}

// Keep the transaction safe even if an initializer forgets to invoke the early activation callback after returning true.
backendActive = true;
// Keep the transaction safe even if an initializer returns true without invoking the native-bridge completion callback.
nativeBridgeCompleted = true;
completeSetup();
return true;
}
catch
{
if (backendActive)
if (nativeBridgeCompleted)
{
try
{
rollback();
}
catch (Exception rollbackFailure)
{
// Rollback is best-effort. Preserve the setup exception, which is the actionable failure, while still allowing a contained diagnostic.
if (reportRollbackFailure != null)
{
try
{
reportRollbackFailure(rollbackFailure);
}
catch (Exception)
{
// Diagnostics must never replace the setup failure.
}
}
}
TryRollback(rollback, reportRollbackFailure);
}
throw;
}
}

private static void TryRollback(
Action rollback,
Action<Exception> reportRollbackFailure)
{
try
{
rollback();
}
catch (Exception rollbackFailure)
{
// Rollback is best-effort. Preserve the initialization outcome while still allowing a contained diagnostic.
if (reportRollbackFailure == null)
{
return;
}

try
{
reportRollbackFailure(rollbackFailure);
}
catch (Exception)
{
// Diagnostics must never replace or escape the original initialization outcome.
}
}
}

/// <summary>
/// Deletes every non-zero JNI local reference in the supplied order.
/// A failed deletion does not prevent later references from being released;
Expand Down
Loading
Loading